[CVE-2022-2633] WordPress All-in-One插件SSRF+任意文件下载

WordPress的All-in-One视频库插件存在任意用户的任意文件下载和SSRF的攻击

漏洞简介

CVE编号:CVE-2022-2633

漏洞描述:WordPress的All-in-One视频库插件存在任意用户的任意文件下载和SSRF的攻击

影响版本:2.5.8~2.6.0

The All-in-One Video Gallery plugin for WordPress is vulnerable to arbitrary file downloads and blind server-side request forgery via the ‘dl’ parameter found in the ~/public/video.php file in versions up to, and including 2.6.0. This makes it possible for unauthenticated users to download sensitive files hosted on the affected server and forge requests to the server.

漏洞分析

环境搭建

首先下载到存在漏洞的版本:https://downloads.wordpress.org/plugin/all-in-one-video-gallery.2.6.0.zip

安装好插件即可

image-20221111153639984

漏洞分析

SSRF

通过漏洞描述知道,漏洞存在~/public/video.php中的参数dl中

如果dl不是数字,就会把内容base64解码,存入$file变量,如果文件为空则exit

image-20221111154125797

然后就是把file中空格给换成20%

image-20221111154234372

下面的判断逻辑是这样的

首先判断home_url()是否存在,这个函数返回的是WordPress 安装的完整 URL

再判断file是否包含http或者https

 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
		// Detect the file type	
		if ( strpos( $file, home_url() ) !== false ) {
			$is_remote_file = false;
		}		        		
          
        if ( preg_match( '#http://#', $file ) || preg_match( '#https://#', $file ) ) {
          	$formatted_path = 'url';
        } else {
          	$formatted_path = 'filepath';
        }

		if ( $is_remote_file ) {
			$formatted_path = 'url';
		}
        
        if ( $formatted_path == 'url' ) {
          	$file_headers = @get_headers( $file );
  
          	if ( $file_headers[0] == 'HTTP/1.1 404 Not Found' ) {
           		die( esc_html__( 'File is not readable or not found.', 'all-in-one-video-gallery' ) );
           		exit;
          	}          
        } elseif ( $formatted_path == 'filepath' ) {		
          	if ( ! @is_readable( $file ) ) {
				die( esc_html__( 'File is not readable or not found.', 'all-in-one-video-gallery' ) );
               	exit;
          	}
        }

如果file包含了http或者https,那么这里$formatted_path = 'url'

这样下面的判断,$is_remote_file默认为true、formatted_path刚刚被赋值为url

判断就会为true从而进入里面执行curl_exec发起请求

 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
       	// Fetching File Size Located in Remote Server
       	if ( $is_remote_file && $formatted_path == 'url' ) {         
          	$data = @get_headers( $file, true );
          
          	if ( ! empty( $data['Content-Length'] ) ) {
          		$file_size = (int) $data[ 'Content-Length' ];          
          	} else {               
               	// If get_headers fails then try to fetch fileSize with curl
               	$ch = @curl_init();

               	if ( ! @curl_setopt( $ch, CURLOPT_URL, $file ) ) {
                 	@curl_close( $ch );
                 	@exit;
               	}
               
               	@curl_setopt( $ch, CURLOPT_NOBODY, true );
               	@curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
               	@curl_setopt( $ch, CURLOPT_HEADER, true );
               	@curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
               	@curl_setopt( $ch, CURLOPT_MAXREDIRS, 3 );
               	@curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
               	@curl_exec( $ch );
               
               	if ( ! @curl_errno( $ch ) ) {
                	$http_status = (int) @curl_getinfo( $ch, CURLINFO_HTTP_CODE );
                    if ( $http_status >= 200 && $http_status <= 300 )
                    	$file_size = (int) @curl_getinfo( $ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD );
               	}

               	@curl_close( $ch );               
          	}          
		} else {         
		   	if ( $formatted_path == 'url' ) {		   
			   $data = @get_headers( $file, true );
			   $file_size = (int) $data['Content-Length'];			   
		   	} elseif ( $formatted_path == 'filepath' ) {		   
		       $file_size = (int) @filesize( $file );			   			   
		   	}		   
       	}

这里测试一个baidu.com,可以看到成功发包

image-20221111155455177

任意文件读取

其实任意文件读取有两条路可以走

  • SSRF打file协议
  • 绕过SSRF

file协议这里其实就简单了,换个协议罢了,比如我们写个file:///etc/passwd

image-20221111160844800

验证成功

image-20221111160715632

那么绕过SSRF的方法是怎么实现的?

在文件末尾可以看到,这里直接进行文件读取然后直接print出来了

1
2
3
4
5
6
7
$nfile = @fopen( $file, 'rb' );
while ( ! feof( $nfile ) ) {                 
    print( @fread( $nfile, $chunk ) );
    @ob_flush();
    @flush();
}
@fclose( $filen );		

所以为了让程序执行到这里,我们就不能让他在前面被截停,从代码上来就是得绕过这个判断

1
if ( $is_remote_file && $formatted_path == 'url' ) 

$is_remote_file是有一个点可以赋值为false的,只要我们的file里面有wordpress的路径就可以了

1
2
3
if ( strpos( $file, home_url() ) !== false ) {
			$is_remote_file = false;
}

这里我传一个

1
file:///http://127.0.0.1/0day/wordpress/wordpress6.1/../../../../../../../etc/passwd

$is_remote_file成功赋值为false

image-20221111165235486

成功readfile

image-20221111165322374

image-20221111165447176

漏洞修复

下载最新版来compare一下:https://downloads.wordpress.org/plugin/all-in-one-video-gallery.2.6.1.zip

修复点1

变量名换成vdl,base64_decode换成wordpress自带的函数

sanitize_text_field,作用是对用户输入进行过滤

Check whether the string is a valid UTF-8 character, and remove all HTML tags

get_transient,用来获取瞬态数据的值,如果瞬态数据不存在、没有值或已过期,则返回值将为false

If the transient does not exist, does not have a value, or has expired, then the return value will be false.

image-20221111172038267

这里如果传入我们的SSRF想到达的URL,就会过不了get_transient了

image-20221111174807777

image-20221111174840733

修复点2

formatted_path设置默认值url

并且判断如果不包含httphttps的话,会把$is_remote_file设为false

image-20221111172211070

修复点3

会取出file的文件后缀ext,然后进行判断

image-20221111172618590

如果不是指定的媒体文件,mime_type会被赋值为application/octet-stream,从而进入判断exit进程

image-20221111172655265

这里就防止了通过下面的fopen来读取文件了

Licensed under CC BY-NC-SA 4.0