在复现历史漏洞的过程中,我发现了一个新的安全问题,这个发现距离现在已有近半年时间。通过这次经历,我深刻体会到在复现漏洞时仔细阅读代码的重要性。
漏洞公告链接:
https://security.yonyou.com/#/noticeInfo?id=400
补丁信息链接:
https://security.yonyou.com/#/patchInfo?identifier=fac37cb5188a4c93bcf5abd0de1336e4
首先分析2023年的反序列化漏洞补丁。令人意外的是,这个补丁的效果并不理想,几乎等同于没有修复。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doAction(request, response);
}
public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ObjectInputStream in = null;
try {
in = new ObjectInputStream(request.getInputStream()) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
Map<String, String> map = new HashMap<String, String>();
map.put(String.class.getName(), String.class.getName());
map.put(HashMap.class.getName(), HashMap.class.getName());
if (!map.containsValue(desc.getName())) {
throw new IllegalArgumentException("�������Ͳ�ƥ��");
}
Class<?> superImpl = super.resolveClass(desc);
return superImpl;
}
};
HashMap<String, String> headInfo = (HashMap<String, String>)in.readObject();
String dsName = headInfo.get("dsName");
InvocationInfoProxy.getInstance().setUserDataSource(dsName);
String oper = headInfo.get("operType");
if ("upload".equals(oper)) {
doUploadFile(headInfo,in,response);
} else if ("download".equals(oper)) {
doDownLoadFile(headInfo, response);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
if(in != null){
in.close();
}
}
}
漏洞点分析:
.readObject()
方法个人反思:
在复现该漏洞时,我并未详细分析补丁内容。由于当时刚接触代码审计,技术水平有限(现在仍需提升),未能深入理解问题本质。不过,注意到修复后仍存在反序列化点,我开始思考是否存在绕过可能性,这为后续发现埋下了伏笔。
// 完整代码见原文,此处展示关键部分
public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ObjectInputStream in = null;
try {
in = new FilteredObjectInputStream(request.getInputStream(), new Class[]{HashMap.class});
HashMap<String, Object> headInfo = (HashMap)in.readObject();
String dsName = (String)headInfo.get("dsName");
InvocationInfoProxy.getInstance().setUserDataSource(dsName);
String oper = (String)headInfo.get("operType");
if ("upload".equals(oper)) {
this.doUploadFile(headInfo, in, response);
} else if ("download".equals(oper)) {
this.doDownLoadFile(headInfo, response);
} else if ("downloadlocal".equals(oper)) {
this.doDownLoadFileLocal(headInfo, response);
}
} catch (Exception var10) {
// 异常处理
} finally {
// 资源清理
}
}
攻击向量:
readObject()
后才能触发operType
参数doDownLoadFileLocal
方法执行文件操作private void doDownLoadFileLocal(HashMap<String, Object> headInfo, HttpServletResponse response) {
String path = (String)headInfo.get("path");
OutputStream out = null;
InputStream in = null;
try {
String ctxPath = RuntimeEnv.getInstance().getCanonicalNCHome();
String realPath = ctxPath + File.separator + "mpEA" + File.separator;
File file = new File(path);
if (!file.getCanonicalPath().startsWith((new File(realPath)).getCanonicalPath())) {
throw new IllegalArgumentException("Prohibit access to this folder");
}
// 文件操作代码...
} catch (Exception var19) {
// 异常处理
} finally {
// 资源清理
}
}
修复措施:
${NCHome}/mpEA/
目录下绕过思路:
虽然目录穿越看似难以绕过,但注意到文件读取操作前存在 .readObject()
调用,且用友历史补丁质量普遍不高,这启发了反序列化绕过的尝试。
public class FilteredObjectInputStream extends ObjectInputStream {
private boolean rootChecked = false;
private boolean fullMatch = false;
private Class<?>[] whiteList;
private static Set<String> blackList;
// 初始化黑名单
static{
File blacks = new File(RuntimeEnv.getInstance().getNCHome(),
"/ierp/security/unserializeBlacklist.conf");
// 黑名单加载逻辑...
}
@Override
protected Class<?> resolveClass(ObjectStreamClass arg0) throws IOException,
ClassNotFoundException {
Class<?> clazz = super.resolveClass(arg0);
if(clazz == null) return clazz;
// 非全匹配模式下的校验逻辑
if(!fullMatch){
// 黑名单校验
if(blackList!=null&&blackList.size()>0&&blackList.contains(clazz.getName())){
throw new IllegalArgumentException("####### class::" + clazz);
}
// 关键漏洞:rootChecked 为 true 时跳过后续校验
if(rootChecked){
return clazz;
}
}
// 白名单校验逻辑
if(whiteList!=null){
for(Class white:whiteList){
if(white.isAssignableFrom(clazz)){
rootChecked =true; // 设置标志位
return clazz; // 直接返回,结束校验
}
}
throw new IllegalArgumentException("####### class::" + clazz);
}
return clazz;
}
}
white.isAssignableFrom(clazz)
匹配成功时:
rootChecked = true
攻击链构造:
使用基于 POJO 改造的攻击链,确保最后一个类为 HashMap,成功实现命令执行,弹出计算器。
通过分析用友系统的历史漏洞和补丁,发现了一个由于校验逻辑不完整导致的反序列化绕过漏洞。这个案例再次证明:
个人感想: 虽然成功发现了漏洞,但对于未能及时公开利用感到遗憾。这次经历让我更加认识到漏洞挖掘的挑战和机遇。