type
Post
status
Published
date
May 25, 2023
slug
128
summary
yonyou nc6.5存在任意类调用执行漏洞,可导致反序列化和命令执行
tags
Java
category
漏洞分析
icon
password
漏洞编号
CNVD-2021-30167
No.
同步状态
状态
已完成
Author
 

前言

用友NC是一款企业级ERP软件。作为一种信息化管理工具,用友NC提供了一系列业务管理模块,包括财务会计、采购管理、销售管理、物料管理、生产计划和人力资源管理等,帮助企业实现数字化转型和高效管理。
用友NC6.5中的nc/bs/framework/server/InvokerServlet.class 存在未授权任意类调用漏洞,该漏洞通过构造特殊的数据包调用NC自有类可导致反序列化或命令执行。其中可利用BeanShell接口执行任意命令,漏洞编号CNVD-2021-30167

安装

windows10 x64 jdk1.7.0_21
下载nc6.5,解压后启动安装脚本 path\NC6.5\yonyou_nc\setup.bat
1、全选安装产品
notion image
2、安装完成后自动启动path\yonyou\home\bin\sysConfig.bat
1)配置服务器信息,点击读取,修改需要自定义的参数后保存
notion image
2)这里不添加任何数据源(数据库)和授权,直接 部署EJB 即可
notion image
3)运行path\yonyou\home\startServer.bat 无报错即可
notion image

准备

导出目录下所有jar包,加载到idea中,配置idea远程调试参数
notion image
在启动yonyounc时添加jdwp参数
C:\java\jdk1.7.0_21\bin\java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=192.168.249.1:5005 -server -Xmx768m -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.awt.headless=true -Dfile.encoding=GBK -Duser.timezone=GMT+8 -Dnc.server.name=server -Dnc.server.startCount=0 -DNC_JAVA_HOME=$JAVA_HOME -Dorg.owasp.esapi.resources=C:\yonyou\home/ierp/bin/esapi -Dnc.bs.logging.format=text -Dnc.server.location=C:\yonyou\home -Drun.side=server -Dnc.run.side=server -cp C:\yonyou\home\starter.jar;C:\java\jdk1.7.0_21\lib\tools.jar;C:\yonyou\home\ant\lib\ant-launcher.jar;C:\yonyou\home\lib\cnytiruces.jar nc.bs.mw.start.AloneBootstrap start
notion image

调试

  • servlet-mapping
notion image
  • poc
notion image

moduleName

根据poc在 bsh/servlet/BshServlet.class$doGet() 方法下断点,跟进到 nc/bs/framework/server/InvokerServlet.class$doAction() 方法
关键代码
pathInfo = pathInfo.trim(); String moduleName = null; String serviceName = null; int beginIndex; if (pathInfo.startsWith("/~")) { //按/~分割字符获取moduleName和serviceName moduleName = pathInfo.substring(2); beginIndex = moduleName.indexOf("/"); if (beginIndex >= 0) { //如果第三个字符之后有斜杠则斜杠之前为moduleName,后为serviceName serviceName = moduleName.substring(beginIndex); if (beginIndex > 0) { moduleName = moduleName.substring(0, beginIndex); } else { moduleName = null; } } else { moduleName = null; serviceName = pathInfo; //如果无斜杠则moduleName为空,pathInfo作为serviceName } } else { serviceName = pathInfo; } if (serviceName == null) { throw new ServletException("Service name is not specified"); } beginIndex = serviceName.indexOf("/"); if (beginIndex < 0 || beginIndex >= serviceName.length() - 1) { throw new ServletException("Service name is not specified"); } serviceName = serviceName.substring(beginIndex + 1); Object obj = null; String msg; try { obj = this.getServiceObject(moduleName, serviceName); //获取服务对象 } catch (ComponentException var76) { msg = svcNotFoundMsgFormat.format(new Object[]{serviceName}); Logger.error(msg, var76); throw new ServletException(msg); }
pathInfo
notion image
传参结果,moduleNamealiserviceName/bsh.servlet.BshServlet
notion image
跟进 this.getServiceObject() 方法
private Object getServiceObject(String moduleName, String serviceName) throws ComponentException { Object retObject = null; if (moduleName == null) { retObject = NCLocator.getInstance().lookup(serviceName); //模块名为空则调用此方法获取服务对象 } else { retObject = serviceObjMap.get(moduleName + ":" + serviceName); //从缓存中获取服务对象 if (retObject == null) { //如果缓存中没有 Container deployed = BusinessAppServer.getInstance().getContainer(moduleName); //根据模块名获取对应的容器对象 try { ........ } if (retObject != null) { //retObject不为空 serviceObjMap.put(moduleName + ":" + serviceName, retObject); } } return retObject; //返回retObject }
notion image
BusinessAppServer.getInstance().getContainer() 中,根据模块名在nameModulesMap中获取对应的容器,228个模块名可利用
notion image
初始化方法中将/home/modules目录下的所有子目录放入HashMap
notion image
notion image

ServiceName兵分两路

getServiceObject之后兵分两路,主要分为两个方法触发漏洞

Service

objBshServlet@12592
接着启动线程监控器跟踪指定线程
ThreadTracer.getInstance().startThreadMonitor("invokeservlet-" + serviceName + "-" + (obj == null ? "" : obj.getClass().getName()), request.getRemoteAddr() + ":" + request.getRemotePort(), "anonymous", (String)null); if (obj instanceof Servlet) { //判断obj是否实现了Servlet接口,是 Logger.init(obj.getClass()); //初始化Logger try { if (obj instanceof GenericServlet) { //是 ((GenericServlet)obj).init(); //初始化 } this.preRemoteProcess(); //预处理 ((Servlet)obj).service(request, response); //调用obj的service()方法处理http请求 this.postRemoteProcess(); } catch (ServletException var72) { this.postErrorRemoteProcess(var72); Logger.error("Invoker serlet: " + obj.getClass() + " error", var72); throw var72; } catch (IOException var73) { this.postErrorRemoteProcess(var73); Logger.error("Invoker serlet: " + obj.getClass() + " error", var73); throw var73; } catch (Throwable var74) { this.postErrorRemoteProcess(var74); Logger.error("Invoker serlet: " + obj.getClass() + " error", var74); throw new ServletException("Invoker serlet: " + obj.getClass() + " error", var74); } finally { Logger.reset(); } }
obj对象BshServlet继承了HttpServlet 类,重写了doGet() 方法
notion image
跟进service方法:javax/servlet/http/HttpServlet.class$service()
serlvetMappings如下
notion image
notion image
经过 org/apache/catalina/connector/RequestFacade.class$getMethod() 方法之后调用当前对象BshServletdoGet方法处理
notion image
注意,如果是POST方法,同样是传递到doGet中执行
notion image
bsh/servlet/BshServlet.class$doGet()
public void doGet(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException { String var3 = var1.getParameter("bsh.script"); //获取执行的脚本 String var4 = var1.getParameter("bsh.client"); String var5 = var1.getParameter("bsh.servlet.output"); String var6 = var1.getParameter("bsh.servlet.captureOutErr"); boolean var7 = false; if (var6 != null && var6.equalsIgnoreCase("true")) { var7 = true; } Object var8 = null; Exception var9 = null; StringBuffer var10 = new StringBuffer(); if (var3 != null) { try { var8 = this.evalScript(var3, var10, var7, var1, var2); //调用evalScript方法处理脚本 } catch (Exception var12) { var9 = var12; } } var2.setHeader("Bsh-Return", String.valueOf(var8)); if ((var5 == null || !var5.equalsIgnoreCase("raw")) && (var4 == null || !var4.equals("Remote"))) { this.sendHTML(var1, var2, var3, var9, var8, var10, var7); } else { this.sendRaw(var1, var2, var9, var8, var10); } }
evalScript方法
Object evalScript(String var1, StringBuffer var2, boolean var3, HttpServletRequest var4, HttpServletResponse var5) throws EvalError { ByteArrayOutputStream var6 = new ByteArrayOutputStream(); PrintStream var7 = new PrintStream(var6); Interpreter var8 = new Interpreter((Reader)null, var7, var7, false); //创建BeanShell解释器实例 var8.set("bsh.httpServletRequest", var4); var8.set("bsh.httpServletResponse", var5); Object var9 = null; Object var10 = null; PrintStream var11 = System.out; PrintStream var12 = System.err; if (var3) { System.setOut(var7); //输出流重定向到PrintStream对象 System.setErr(var7); } try { var9 = var8.eval(var1); //执行命令 } finally { if (var3) { System.setOut(var11); System.setErr(var12); } } var7.flush(); var2.append(var6.toString()); return var9; }
跟进到 Interpreter.class$eval() 方法
public Object eval(String var1) throws EvalError { if (DEBUG) { debug("eval(String): " + var1); } return this.eval(var1, this.globalNameSpace); } public Object eval(String var1, NameSpace var2) throws EvalError { String var3 = var1.endsWith(";") ? var1 : var1 + ";"; return this.eval(new StringReader(var3), var2, "inline evaluation of: ``" + this.showEvalString(var3) + "''"); }
notion image
最后调用不同参数类型的eval方法
notion image
该方法创建一些新的BeanShell解释器实例,并将输入输出流、命令空间、调用堆栈等传入,逐行读取BeanShell脚本
并调用 BSHPrimaryExpression.class$eval() 方法执行命令,最后返回执行的结果var4
private Object eval(boolean var1, CallStack var2, Interpreter var3) throws EvalError { Object var4 = this.jjtGetChild(0); int var5 = this.jjtGetNumChildren(); for(int var6 = 1; var6 < var5; ++var6) { var4 = ((BSHPrimarySuffix)this.jjtGetChild(var6)).doSuffix(var4, var1, var2, var3); } if (var4 instanceof SimpleNode) { if (var4 instanceof BSHAmbiguousName) { if (var1) { var4 = ((BSHAmbiguousName)var4).toLHS(var2, var3); } else { var4 = ((BSHAmbiguousName)var4).toObject(var2, var3); } } else { if (var1) { throw new EvalError("Can't assign to prefix.", this, var2); } var4 = ((SimpleNode)var4).eval(var2, var3); } } if (var4 instanceof LHS) { if (var1) { return var4; } else { try { return ((LHS)var4).getValue(); } catch (UtilEvalError var8) { throw var8.toEvalError(this, var2); } } } else { return var4; } }
notion image
BSHPrimaryExpression.class$eval()方法解析出解释器对象要执行的脚本参数,反射调用exec
notion image
bsh/Name.class$invokeMethod中调用本地方法bsh/Name.class$invokeLocalMethod ,var6获取var2的类型为String,调用getCommand 方法
notion image
var7获取到执行的脚本位置/bsh/commands/exec.bsh,通过 BshClassManager.getClassManager().getResourceAsStream() 读取到该脚本内容到InputStream,之后调用bsh/NameSpace.class$loadScriptedCommand 方法加载脚本
notion image
exec.bshRuntime.getRuntime().exec执行传入的参数
notion image
后续简单看一下调用栈
返回到invokeLoaclMethod,var10为null,进入var9.invoke
notion image
跟进bsh/BshMethod.class$invoke()
notion image
notion image
invokeImpl()
notion image
private Object invokeImpl(Object[] var1, Interpreter var2, CallStack var3, SimpleNode var4, boolean var5) throws EvalError { Class var6 = this.getReturnType(); Class[] var7 = this.getParameterTypes(); if (var3 == null) { var3 = new CallStack(this.declaringNameSpace); } if (var1 == null) { var1 = new Object[0]; } if (var1.length != this.numArgs) { throw new EvalError("Wrong number of arguments for local method: " + this.name, var4, var3); } else { NameSpace var8; if (var5) { var8 = var3.top(); } else { var8 = new NameSpace(this.declaringNameSpace, this.name); var8.isMethod = true; } var8.setNode(var4); for(int var9 = 0; var9 < this.numArgs; ++var9) { if (var7[var9] != null) { try { var1[var9] = Types.getAssignableForm(var1[var9], var7[var9]); } catch (UtilEvalError var17) { throw new EvalError("Invalid argument: `" + this.paramNames[var9] + "'" + " for method: " + this.name + " : " + var17.getMessage(), var4, var3); } try { var8.setTypedVariable(this.paramNames[var9], var7[var9], var1[var9], (Modifiers)null); } catch (UtilEvalError var16) { throw var16.toEvalError("Typed method parameter assignment", var4, var3); } } else { if (var1[var9] == Primitive.VOID) { throw new EvalError("Undefined variable or class name, parameter: " + this.paramNames[var9] + " to method: " + this.name, var4, var3); } try { var8.setLocalVariable(this.paramNames[var9], var1[var9], var2.getStrictJava()); } catch (UtilEvalError var15) { throw var15.toEvalError(var4, var3); } } } if (!var5) { var3.push(var8); //命令空间对象压入堆栈 } Object var10 = this.methodBody.eval(var3, var2, true); //调用方法体eval CallStack var11 = var3.copy(); if (!var5) { var3.pop(); } ReturnControl var12 = null; if (var10 instanceof ReturnControl) { var12 = (ReturnControl)var10; if (var12.kind != 46) { throw new EvalError("'continue' or 'break' in method body", var12.returnPoint, var11); } var10 = ((ReturnControl)var10).value; if (var6 == Void.TYPE && var10 != Primitive.VOID) { throw new EvalError("Cannot return value from void method", var12.returnPoint, var11); } } if (var6 != null) { if (var6 == Void.TYPE) { return Primitive.VOID; } try { var10 = Types.getAssignableForm(var10, var6); } catch (UtilEvalError var18) { SimpleNode var14 = var4; if (var12 != null) { var14 = var12.returnPoint; } throw var18.toEvalError("Incorrect type returned from method: " + this.name + var18.getMessage(), var14, var3); } } return var10; } }
这段代码定义了一个BeanShell解释器中的类BSHMethodInvocation,用于处理方法调用的执行。
invokeImpl方法中,首先获取方法返回值和参数类型,然后判断参数个数是否匹配。如果匹配,则创建一个新的命名空间对象,并设置方法参数的值。在设置方法参数的值时,如果参数类型不为空,则调用Types.getAssignableForm方法将参数转化为可赋值的形式,并调用setTypedVariable方法设置参数的值;否则,直接调用setLocalVariable方法设置参数的值。
然后,将新的命名空间对象压入调用堆栈中,并调用方法体的eval方法计算方法体的值。在计算方法体的值时,如果方法体返回一个ReturnControl对象,则将其转化为返回值,并执行相应的控制流程(如continue和break)。如果方法返回值的类型不为空,将返回值转化为可赋值的形式,并返回该值。如果返回值的类型为空,则返回Primitive.VOID对象。
 
接着执行代码块
notion image
最后调用 BSHPrimaryInvocation.class$eval() 执行 exec.bsh 中的命令

doAction

/servlet/~ic/nc.bs.framework.mx.monitor.MonitorServlet 这个payload为例
同样在InvokerServlet.class$doAction() 中通过getServiceObject获取到obj为MonitorServlet对象
notion image
之后进入adaptor.doAction ,该方法中提供了反序列化输入流的操作readObject
notion image

Reference

 
巧用snort规则关键字,让入侵检测事半功倍MiniCMS代码执行漏洞