Critical Vulnerabilities in Adning Advertising Plugin Patched, over 8,000 customers affected

The WordFence Threat Intelligence team was made aware of a possible vulnerability in the Adning Advertising plugin, a premium plugin with over 8,000 customers developed by plugin’s author, Tunafish. They discovered 2 vulnerabilities, one of which was a critical vulnerability that allowed an unauthenticated attacker to upload arbitrary files, leading to Remote Code Execution(RCE), which could allow complete site takeover.

How the vulnerabilities were engaged

Description: Unauthenticated Arbitrary File Upload leading to Remote Code Execution
Affected Plugin: Adning Advertising
Plugin Slug: angwp
Affected Versions: < 1.5.6
CVE ID: N/A
CVSS Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVSS score: 10.0(critical)
Patched Version: 1.5.6

One functionality of the Adning plugin is to allow users to upload banner images. In order to provide this functionality, it used an AJAX action, _ning_upload_image. Unfortunately this AJAX action was available with a nopriv_ hook, meaning that any visitor to the site could make use of it, even if they were not logged in. Additionally, the function called by this AJAX action also failed to make use of a capability check or a nonce check.

115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
public static function _ning_upload_image()
{
    $_action = isset($_POST['action']) ? $_POST['action'] : '';
    $user_id = isset($_POST['uid']) ? $_POST['uid'] : 0;
    $banner_id = isset($_POST['bid']) ? $_POST['bid'] : 0;
    $max_upload_size = isset($_POST['max_upload_size']) ? $_POST['max_upload_size'] : 100;
    $upload = isset($_POST['upload']) ?  json_decode(stripslashes($_POST['upload']), true) : array();
    $valid_formats = isset($_POST['allowed_file_types']) ? explode(',', $_POST['allowed_file_types']) : array('jpg');
    if( in_array('jpg', $valid_formats) )
    {
        $valid_formats[] = 'jpeg';
    }
    
    //$max_file_size = 1024*100; //100 kb
    //$max_file_size = 1024000*15; // 15 MB (1 mb = 1000 kb)
    $max_file_size = 1024000*$max_upload_size;
    
    //$upload_path = $upload_dir.'/'.$upload_folder;
    //$upload_path = $upload_path.$upload_folder;
    $upload_path = $upload['dir'].$upload['folder'];
    $count = 0;
    // Create upload folder if not exists
    if(!is_dir($upload_path)) {
        mkdir($upload_path, 0777, true);
    }
    if(!empty($_FILES['files']))
    {
        $upload_success = false;
        $upload_error = '';
        $uploaded_files = array();
        $unzip_error = array();
        // Loop $_FILES to execute all files
        foreach ($_FILES['files']['name'] as $f => $name)
        {    
            if ($_FILES['files']['error'][$f] == 4)
            {
                continue; // Skip file if any error found
            }         
            if ($_FILES['files']['error'][$f] == 0)
            {             
                if ($_FILES['files']['size'][$f] > $max_file_size)
                {
                    $upload_error = $name. " is too large!";
                    continue; // Skip large files
                }
                elseif( !in_array(pathinfo($name, PATHINFO_EXTENSION), $valid_formats) )
                {
                    $upload_error = $name." is not a valid format";
                    continue; // Skip invalid file formats
                }
                else
                {
                    // No error found! Move uploaded files
                    if(move_uploaded_file($_FILES["files"]["tmp_name"][$f], $upload_path.$name)){
                        $count++; // Number of successfully uploaded file
                        $src = $upload['src'].$upload['folder'].$name;
                        // Copy image to banner folder
                        /*if(!empty($banner_id))
                        {
                            if(!is_dir($upload_dir.'/'.$banner_folder)) {
                                mkdir($upload_dir.'/'.$banner_folder, 0777, true);
                            }
                            copy($path.$name, $upload_dir.'/'.$banner_folder.$name);
                        }*/
                        $uploaded_files[] = array(
                            'name' => $name,
                            'size' => $_FILES['files']['size'][$f],
                            'upload' => $upload,
                            'path' => $upload_path.$name,
                            'src' => $src,
                            'grid_item' => '<div class="grid-item" data-src="'.$src.'" data-use="path"><img src="'.$src.'" /><div class="info_btn" data-info="'.basename($src).'"><svg viewBox="0 0 448 512" style="height:18px;border-radius:2px;"><path fill="currentColor" d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zm-176 86c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" class=""></path></svg></div></div>',
                            'uid'  => $user_id,
                            'action'  => $_action
                        );
                        
                        
                        if( pathinfo($name, PATHINFO_EXTENSION) == 'zip')
                        {
                            $zipfile = array(
                                'name' => $_FILES['files']['name'][$f],
                                'type' => $_FILES['files']['type'][$f],
                                'tmp_name' => $_FILES['files']['tmp_name'][$f],
                                'error' => $_FILES['files']['error'][$f],
                                'size' => $_FILES['files']['size'][$f],
                            );
                            
                            $unzip_error = self::upload_and_unzip($zipfile, array('folder' => $upload['folder'], 'path' => $upload_path, 'src' => $upload['src']));
                        }
                    }
                    else
                    {
                        $upload_error = is_writable($upload_path) ? 'Could not move files.' : 'Folder is not writable.';
                    }
                }
            }
        }
        if(count($uploaded_files) > 0){
            $upload_success = true;
        }
        echo json_encode(array("chk" => $_FILES['files'], "unzip" => $unzip_error, "upload" => $upload, "success" => $upload_success, "files" => json_encode($uploaded_files), "error" => $upload_error));
    }else{
        echo 'no files found.';
    }
    exit;
}

This function also allowed the user to supply the “allowed” file types. As such it was possible for an unauthenticated attacker to upload malicious code by sending a POST request to wp-admin/admin-ajax.php with the action parameter set to _ning_upload_image the allowed_file_types set to php, and a files parameter containing a malicious PHP file. Alternatively, an attacker could set the allowed_file_types to zip and upload a compressed archive containing a malicious PHP file, which would be unzipped after upload. It was also possible for an attacker to change the upload directory by manipulating the contents of the upload parameter – if the desired directory did not exist, the plugin would create it.


Description: Unauthenticated Arbitrary File Deletion via path traversal
Affected Plugin: Adning Advertising
Plugin Slug: angwp
Affected Versions: < 1.5.6
CVE ID: N/A
CVSS Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:H
CVSS score: 8.7(high)
Patched Version: 1.5.6

In order to delete any uploaded images, the plugin also registered another ajax action, _ning_remove_image, which also used a nopriv_ hook. As with the upload vulnerability, this function did not perform a capability check or a nonce check. As such it was possible for an unauthenticated attacker to delete arbitrary files using path traversal.

If an attacker were able to delete wp-config.php, the site would be reset, and an attacker could then set it up again and point it to a remote database under their control, effectively replacing the site’s content with their own content.

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
public static function _ning_remove_image()
{
    $upload = wp_upload_dir();
    $upload_dir = $upload['basedir'];
    $upload_url = $upload['baseurl'];
    $upload_folder = self::$upload_folder.$_POST['uid'].'/';   
    $path = $upload_dir.'/'.$upload_folder.basename($_POST['src']);
    $removed = 0;
    if(unlink($path)){
        $remove = 1;
    }
    echo $remove;
    exit;
}

This attack might require an extra step of preparation, which is that the wp-content/uploads/path folder would need to exist. However, since the previously mentioned arbitrary file upload vulnerability allowed for directory creation, this was not a major obstacle. Once the directory was created, an attacker could send a POST request to wp-admin/admin-ajax.php with the action parameter set to _ning_remove_image, the uid parameter set to /../../.. and the src parameter set to wp-config.php.

Timeline

June 24, 2020 – Wordfence Threat Intelligence receives a report of a compromised website running the Adning plugin. During our investigation, we discovered two vulnerabilities.
June 25, 2020 – Firewall rule released for Premium Wordfence users. We make initial contact with plugin’s author and send full disclosure after receiving a response.
June 26, 2020 – Plugin’s author releases a patch.
July 25, 2020 – Firewall rule becomes available to Wordfence free users.

Conclusion

These two flaws have been fully patched in version 1.5.6. If you are running this plugin, it is critical that you updated to this version as soon as possible. Sites running Wordfence Premium have been protected against these vulnerabilities since June 25, 2020, while sites still using the free version of Wordfence will receive the firewall rule on July 25, 2020. Read more about this on WordFence

A patched version was made available in less than 24 hours, on June 26, 2020. We strongly recommend updating to the latest version of this plugin, 1.5.6, immediately.

Special Thanks to Tunafish, the author of the Adning Advertising plugin, for their excellent and timely response in releasing a patch.

Read more of our WPChase security blogs here

Wonderful!, just before you go: Please subscribe to our website for the latest tips, ideas, and recommendations to make your WordPress site wonderful.

Subscribe to Blog via Email

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 152 other subscribers

%d bloggers like this: