Log4j出现了远程执行漏洞, 直接升级log4j版本不实现(启动会报错,新版的包结构可能有改变), 在github发现一个打补丁的方法, 就是用同名类覆盖JndiLookup类使其实例化报错.
在本地启用idea测试的时候非常顺利,包含Jndi地址的日志不会被解析而是直接打印出来. 于是便发包到服务器测试, 结果事与愿违, 漏洞还是能够触发.这确实不应该啊.
查看包的内容log4j-patch和log4j-core包都在, 所以可以初步确定是classpath顺序的问题. 查看包文件meta-INf/MANIFEST.MF(相当于jar包描述文件)
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: *** Start-Class: ***.Application Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Version: 1.4.5.RELEASE Created-By: Apache Maven 3.6.3 Build-Jdk: 1.8.0_131 Main-Class: org.springframework.boot.loader.JarLauncher
可以发现它的启动类是org.springframework.boot.loader.JarLauncher而jar包中确实有这个类的字节码,
通常,spring-boot-maven-plugin打的Jar包都是通过java -jar ***.jar 直接运行的,并没有直接添加classpath参数,所有可以猜测classpath是在运行jar包时指定的, 于是乎接下来就是debug了, 但是没有启动类org.springframework.boot.loader.JarLauncher源码啊, 这怎么好debug(其实没源码应该也能debug,只是比较麻烦), 想了想这是spring-boot-maven-plugin插件生成的jar包, spring-boot仓库应该是有源码的, 于是在github上一番通过包名一番搜索找到类非常相似的包类名(https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader), github上默认是main(master)分支代码都比较新,所以通过tag切换到我所使用的版本(1.4.5.RELEASE),发现结构几乎和jar包相同, 可以肯定这就是源码了.
接着又面向百度编程找如何debug jar包程序
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar ***.jar
在Idea中Edit Configurations 添加Remote JVM DEBUG配置,并设置源码路径,debug端口
进行debug
org.springframework.boot.loader.JarLauncher入口
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
调用的是父类org.springframework.boot.loader.Launcher的launch(java.lang.String[])方法
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
org.springframework.boot.loader.JarLauncher的父类
org.springframework.boot.loader.ExecutableArchiveLauncher
@Override protected ListgetClassPathArchives() throws Exception { List archives = new ArrayList ( this.archive.getNestedArchives(new EntryFilter() { @Override public boolean matches(Entry entry) { return isNestedArchive(entry); } })); postProcessClassPathArchives(archives); return archives; }
getClassPathArchives获取了所有jar依赖包信息.
其中this.archive是org.springframework.boot.loader.archive.JarFileArchive类实例, 参考org.springframework.boot.loader.ExecutableArchiveLauncher的父类org.springframework.boot.loader.Launcher中createArchive方法
package org.springframework.boot.loader;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.jar.JarFile;
public abstract class Launcher {
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(List archives) throws Exception {
List urls = new ArrayList(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[urls.size()]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
protected abstract String getMainClass() throws Exception;
protected abstract List getClassPathArchives() throws Exception;
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getSchemeSpecificPart());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
}
createClassLoader方法用到了org.springframework.boot.loader.LaunchedURLClassLoader类,而这个类继承了java.net.URLClassLoader, 而org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)方法修改了当前线程的classLoader, 相当于指定了classpath, 接着便是反射调用我们真正的SpringBoot启动类了
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
通过JDK文档得知java.net.URLClassLoader加载类的顺序和构造参数urls顺序相关
public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
this.acc = AccessController.getContext();
ucp = new URLClassPath(urls, acc);
}
而这个urls顺序来自org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives方法得到的List的顺序
接下来看看org.springframework.boot.loader.archive.JarFileArchive#getNestedArchives方法
public ListgetNestedArchives(EntryFilter filter) throws IOException { List nestedArchives = new ArrayList (); for (Entry entry : this) { if (filter.matches(entry)) { nestedArchives.add(getNestedArchive(entry)); } } return Collections.unmodifiableList(nestedArchives); }
它遍历了自己(它实现了java.lang.Iterable)
@Override public Iteratoriterator() { return new EntryIterator(this.jarFile.entries()); }
而这个jarFile就是我们这个jar程序包, 它是org.springframework.boot.loader.jar.JarFile类型, 也是java.util.jar.JarFile的子类
@Override public Enumerationentries() { final Iterator iterator = this.entries.iterator(); return new Enumeration () { @Override public boolean hasMoreElements() { return iterator.hasNext(); } @Override public java.util.jar.JarEntry nextElement() { return iterator.next(); } }; }
其中iterator()方法
@Override public Iteratoriterator() { return new EntryIterator(); }
private class EntryIterator implements Iterator{ private int index = 0; @Override public boolean hasNext() { return this.index < JarFileEntries.this.size; } @Override public JarEntry next() { if (!hasNext()) { throw new NoSuchElementException(); } int entryIndex = JarFileEntries.this.positions[this.index]; this.index++; return getEntry(entryIndex, JarEntry.class, false); } }
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
RandomAccessData data, JarEntryFilter filter, JarFileType type)
throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.pathFromRoot = pathFromRoot;
CentralDirectoryParser parser = new CentralDirectoryParser();
// 重点
this.entries = parser.addVisitor(new JarFileEntries(this, filter));
parser.addVisitor(centralDirectoryVisitor());
this.data = parser.parse(data, filter == null);
this.type = type;
}
这里面的东西有点乱, 主要看下面这个方法(org.springframework.boot.loader.jar.CentralDirectoryParser#parseEntries)
private void parseEntries(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) throws IOException {
byte[] bytes = Bytes.get(centralDirectoryData);
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
int dataOffset = 0;
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
fileHeader.load(bytes, dataOffset, null, 0, null);
visitFileHeader(dataOffset, fileHeader);
dataOffset += this.CENTRAL_DIRECTORY_HEADER_base_SIZE
+ fileHeader.getName().length() + fileHeader.getComment().length()
+ fileHeader.getExtra().length;
}
}
总结一下: List是根据jar程序包中依赖包文件地址顺序而来的, 所以要想知道classpath顺序,得知道打包jar程序包是依赖包的写入文件的顺序, 所以问题出在spring-boot-maven-plugin打包项目上
spring-boot-maven-plugin 打包分析
项目用的是spring-boot-maven-plugin的repackage来打成可执行包, 不多说, 还是debug
maven 打包debug (默认是8000端口)
mvnDebug -DskipTests=true package
关键方法
org.springframework.boot.maven.RepackageMojo#repackage
private void repackage() throws MojoExecutionException {
File source = this.project.getArtifact().getFile();
File target = getTargetFile();
Repackager repackager = getRepackager(source);
// this.project 是maven传递给插件的
// this.project.getArtifacts() 获取项目的所有依赖
Set artifacts = filterDependencies(this.project.getArtifacts(),
getFilters(getAdditionalFilters()));
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
getLog());
try {
Launchscript launchscript = getLaunchscript();
repackager.repackage(target, libraries, launchscript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
updateArtifact(source, target, repackager.getBackupFile());
}
其中 this.project.getArtifacts() 是获取项目的所有依赖, 注意它返回的是linkedHashSet它是有顺序的,而且是按照maven的依赖规则生成的顺序, 但是在我调试的时候filterDependencies方法返回的是HashSet
org.springframework.boot.maven.AbstractDependencyFilterMojo#filterDependencies
protected SetfilterDependencies(Set dependencies, FilterArtifacts filters) throws MojoExecutionException { try { return filters.filter(dependencies); } catch (ArtifactFilterException e) { throw new MojoExecutionException(e.getMessage(), e); } }
通过查看代码发现源码中使用的依赖过滤器返回的都是HashSet
- org.apache.maven.shared.artifact.filter.collection.ScopeFilter
- org.apache.maven.shared.artifact.filter.collection.ArtifactIdFilter
- org.springframework.boot.maven.MatchingGroupIdFilter
- org.springframework.boot.maven.ExcludeFilter
- org.springframework.boot.maven.IncludeFilter
生成Jar包时的依赖包的写入顺序
org.springframework.boot.loader.tools.Repackager
private void repackage(JarFile sourceJar, File destination, Libraries libraries,
Launchscript launchscript) throws IOException {
JarWriter writer = new JarWriter(destination, launchscript);
try {
final List unpackLibraries = new ArrayList();
final List standardLibraries = new ArrayList();
// 重点关注doWithLibraries
libraries.doWithLibraries(new LibraryCallback() {
@Override
public void library(Library library) throws IOException {
File file = library.getFile();
if (isZip(file)) {
if (library.isUnpackRequired()) {
unpackLibraries.add(library);
}
else {
standardLibraries.add(library);
}
}
}
});
writer.writeManifest(buildManifest(sourceJar));
Set seen = new HashSet();
writeNestedLibraries(unpackLibraries, seen, writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar,
new RenamingEntryTransformer(((RepackagingLayout) this.layout)
.getRepackagedClassesLocation()));
}
else {
writer.writeEntries(sourceJar);
}
writeNestedLibraries(standardLibraries, seen, writer);
if (this.layout.isExecutable()) {
writer.writeLoaderClasses();
}
}
finally {
try {
writer.close();
}
catch (Exception ex) {
// Ignore
}
}
}
// doWithLibraries 得到的包按序写入jar文件
private void writeNestedLibraries(List libraries, Set alreadySeen,
JarWriter writer) throws IOException {
for (Library library : libraries) {
String destination = Repackager.this.layout
.getLibraryDestination(library.getName(), library.getScope());
if (destination != null) {
if (!alreadySeen.add(destination + library.getName())) {
throw new IllegalStateException(
"Duplicate library " + library.getName());
}
writer.writeNestedLibrary(destination, library);
}
}
}
org.springframework.boot.maven.ArtifactsLibraries#doWithLibraries是对HashSet进行遍历的,所以写入依赖包的顺序是不确定的, 这就导致使用该插件打的jar包在运行时无法确定依赖的引入顺序,从而导致同包同名类覆盖失效
@Override
public void doWithLibraries(LibraryCallback callback) throws IOException {
// this.artifacts 是 `org.springframework.boot.maven.RepackageMojo#repackage` 传过来的.其类型为HashSet
Set duplicates = getDuplicates(this.artifacts);
for (Artifact artifact : this.artifacts) {
LibraryScope scope = SCOPES.get(artifact.getScope());
if (scope != null && artifact.getFile() != null) {
String name = getFileName(artifact);
if (duplicates.contains(name)) {
this.log.debug("Duplicate found: " + name);
name = artifact.getGroupId() + "-" + name;
this.log.debug("Renamed to: " + name);
}
callback.library(new Library(name, artifact.getFile(), scope,
isUnpackRequired(artifact)));
}
}
}
修复
查看github上spring-boot项目源码, 发现它在 2.0.0.RELEASE及之后的版本修复了这个bug,
但是我没在它的commit中找到提及这个bug的相关信息
org.springframework.boot.maven.RepackageMojo
private void repackage() throws MojoExecutionException {
Artifact source = getSourceArtifact();
File target = getTargetFile();
Repackager repackager = getRepackager(source.getFile());
Libraries libraries = getLibraries(this.requiresUnpack);
try {
Launchscript launchscript = getLaunchscript();
repackager.repackage(target, libraries, launchscript, parseOutputTimestamp());
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
updateArtifact(source, target, repackager.getBackupFile());
}
org.springframework.boot.maven.AbstractPackagerMojo
protected final Libraries getLibraries(Collectionunpacks) throws MojoExecutionException { Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); return new ArtifactsLibraries(artifacts, unpacks, getLog()); }
org.springframework.boot.maven.AbstractDependencyFilterMojo#filterDependencies
protected Set总结filterDependencies(Set dependencies, FilterArtifacts filters) throws MojoExecutionException { try { // dependencies 本身也是linkedHashSet // 过滤这里使用linkedHashSet,这样就和maven原来解析依赖的顺序一致 Set filtered = new linkedHashSet<>(dependencies); filtered.retainAll(filters.filter(dependencies)); return filtered; } catch (ArtifactFilterException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } }
spring-boot-maven-plugin低于2.0.0.RELEASE时打的包是有一定问题的, 它指定classpath的顺序没有按maven的依赖规则, 它只能确保依赖包是那几个, 不能确定依赖包的引入顺序. 对于项目有同包同名类时(先引入的依赖中的类生效),这是有问题的. 对于spring-boot1.x, 如果使用spring-boot-maven-plugin打包最好还是使用2.0.0.RELEASE及以上版本.



