漏洞描述
URL Redirection to Untrusted Site ('Open Redirect') vulnerability when "form" authentication is used in Apache Shiro.
Mitigation:Update to Apache Shiro 1.13.0+ or 2.0.0-alpha-4+.
[1]
漏洞条件
- shiro < 1.13.0
漏洞复现
shiro : 1.12.0
springBoot : 2.7.4
shiro配置:
#shiro.ini
# format: roleName = permission1, permission2, ..., permissionN
[roles]
user = printer:print
admin = printer:*
# format: username = password, role1, role2, ..., roleN
[users]
user1 = pswd123, user
admin1 = pswd321, admin
@Configuration
@Import({ShiroBeanConfiguration.class,
ShiroAnnotationProcessorConfiguration.class,
ShiroWebConfiguration.class,
})
public class ShiroConfig{
@Bean
public IniRealm getIniRealm() {
return new IniRealm("classpath:shiro.ini");
}
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/loginSuccess");
bean.setUnauthorizedUrl("/unauthorized");
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/login", "authc");
map.put("/**","authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}
测试
-
访问恶意路径:
//www.malicious.com
, -
登入(带上上图中的会话id)
可以发现,重定向到了恶意网站。
漏洞分析
漏洞入口:org.apache.shiro.web.filter.authc.FormAuthenticationFilter#onAccessDenied(request,response)
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {//如果是post类型的登入请求,则进入
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
//第二个请求到达这里
return executeLogin(request, response);
} else {//如果是GET请求,则进入这里
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
//第一个请求到达这里
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
为什么会进入这个入口?:
//AccessControlFilter::
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//先判断是否允许访问,如果否则进入onAccessDenied
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
//AuthenticatingFilter (extends AuthenticationFilter)
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
//Permissive是一种特殊的“特权”配置,不用验证,直接通过,我们的配置环境中并没有,所以isPermissive为false
}
//AuthenticationFilter
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
//当前用户是否被验证,该信息是存储在会话中
return subject.isAuthenticated() && subject.getPrincipal() != null;
}
综上可知,同时满足以下条件的会进入漏洞入口:
- 没有被验证
- 不是Permissive
所以毫无疑问,漏洞复现中测试的第一个请求中没有会话id,故必然是符合第一个条件subject.isAuthenticated()==false。
第一个请求到达处
//AccessControlFilter::
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
//底层调用response.sendRedirect(url)
redirectToLogin(request, response);
}
protected void saveRequest(ServletRequest request) {
WebUtils.saveRequest(request);
}
//WebUtils::
public static void saveRequest(ServletRequest request) {
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
HttpServletRequest httpRequest = toHttp(request);
SavedRequest savedRequest = new SavedRequest(httpRequest);
//SAVED_REQUEST_KEY == "shiroSavedRequest"
session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
}
逻辑很简单,就是存储当前的请求到会话中,并重定向到登入请求:符合漏洞复现的第一个请求的响应结果
第二个请求到达处
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//封装请求中的Principal以及Credentials即账户密码
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
//登入失败会抛出异常,故进入此分支
return onLoginFailure(token, e, request, response);
}
}
登入成功后:
//FormAuthenticationFilter (extends AuthenticatingFilter)::
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request, ServletResponse response) throws Exception {
issueSuccessRedirect(request, response);
//we handled the success redirect directly, prevent the chain from continuing:
return false;
}
//AuthenticationFilter::
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
//getSuccessUrl()获取的是配置中的SuccessUrl,我们配置的是"/loginSuccess"
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}
从函数名就可知,是重定向到保存的请求
//WebUtils::
public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
throws IOException {
String successUrl = null;
boolean contextRelative = true;
//从会话中获取之前存储的请求
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
//请求不为空且是GET类型
if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
//获取第一个请求中的“//www.malicious.com”
successUrl = savedRequest.getRequestUrl();
contextRelative = false;
}
//如果第一次请求直接访问登入请求,则不会saveRequestAndRedirectToLogin那么进入此分支
//fallBackUrl传入的是配置中的successUrl
if (successUrl == null) {
successUrl = fallbackUrl;
}
if (successUrl == null) {
throw new IllegalStateException("Success URL not available via saved request or via the " +
"successUrlFallback method parameter. One of these must be non-null for " +
"issueSuccessRedirect() to work.");
}
//重定向到successUrl
//底层调用response.sendRedirect(url)
WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
}
但是为什么第一个请求的重定向的“Location”带有域名“http://localhost:9090"而第二个请求的重定向中的同一字段却没有该域名?
这是因为response.sendRedirect(url),有三种处理方式:[3]
url的情况 | 处理方式 |
---|---|
开头没有”/“ | 相对当前请求 |
开头有”/“ | 相对context-path(应用路径) |
绝对路径 | 直接转发或协议相对 |
引用[3]中“http头不可省略”是错误的,实际情况是如果是以“//”开头那么就是“协议相对”,即相对当前请求所用的协议
上图中encodedRedirectURL中缺少了协议头,所以使用当前请求中的协议”HTTP/1.1"
所以最终会被重定向到“http://www.malicious.com”
总结:
-
原理:第一次恶意请求被保存在会话,而在POST登入请求成功后重定向到第一个恶意请求的恶意路径,从而导致用户访问恶意网站。
-
可能的利用场景:构造更具欺骗性的恶意链接(受害者看到链接域名正常可能会放松警惕)
漏洞修复
在获取被保存请求的请求路径时去除多余的slash
Reference
[1] Security Reports | Apache Shiro
[2] Apache Shiro FORM URL Redirect漏洞(CVE-2023-46750)-CSDN博客
[3] 重定向方法sendRedirect()中的路径问题的初步了解_sendredirect方法-CSDN博客
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)