you can use wp_handle_sideload() method

    // Gives us access to the download_url() and wp_handle_sideload() functions
    require_once( ABSPATH . 'wp-admin/includes/file.php' );
    
    // URL to the WordPress logo
    $url = 'http://s.w.org/style/images/wp-header-logo.png';
    $timeout_seconds = 5;
    
    // Download file to temp dir
    $temp_file = download_url( $url, $timeout_seconds );
    
    if ( !is_wp_error( $temp_file ) ) {
    
    	// Array based on $_FILE as seen in PHP file uploads
    	$file = array(
    		'name'     => basename($url), // ex: wp-header-logo.png
    		'type'     => 'image/png',
    		'tmp_name' => $temp_file,
    		'error'    => 0,
    		'size'     => filesize($temp_file),
    	);
    
    	$overrides = array(
    		// Tells WordPress to not look for the POST form
    		// fields that would normally be present as
    		// we downloaded the file from a remote server, so there
    		// will be no form fields
    		// Default is true
    		'test_form' => false,
    
    		// Setting this to false lets WordPress allow empty files, not recommended
    		// Default is true
    		'test_size' => true,
    	);
    
    	// Move the temporary file into the uploads directory
    	$results = wp_handle_sideload( $file, $overrides );
    
    	if ( !empty( $results['error'] ) ) {
    		// Insert any error handling here
    	} else {
    
    		$filename  = $results['file']; // Full path to the file
    		$local_url = $results['url'];  // URL to the file in the uploads dir
    		$type      = $results['type']; // MIME type of the file
    
    		// Perform any actions here based in the above results
    	}
    
    }