Wednesday, July 14, 2010

smpCTF challenge #11 write-up, phplist 0day

Challenge #11 of smpCTF was interesting: we were given an URL to a phplist version 2.10.12 installation - with default admin/phplist administrator account - and instructed to find a 0day.

SQL injection in phplist 2.10.12

Nibbles teammates sh4ka & Gu1 quickly performed a security audit of phplist (apparently not known for its security) and found an SQL injection in:

On line 57, findby is obtained from user input as a GET parameter, with some filtering against XSS attacks:
$_SESSION["userlistfilter"]["findby"] = removeXss($_GET["findby"]);

removeXss() function is defined on line 21 in:
It simply escapes special HTML chars with htmlspecialchars().

Back to the main file, on line 65 findby is stored in a variable:
$findby = $_SESSION["userlistfilter"]["findby"];
And later on line 82 passed as-is into an SQL query:
$findatt = Sql_Fetch_Array_Query("select id,tablename,type,name from {$tables["attribute"]} where id = $findby");
We have found the vulnerability: one can perform an SQL injection trough findby GET parameter.

Proposed fix: since id is an integer, it seems obvious to me that they should intval(). No need for complex escaping.

Exploiting the SQL injection

We have no output of our SQL injection, it's blind. However they found an interesting way to obtain a binary answer with the following SQL injection:
[...] where id = 0 and if(X, (select table_name from information_schema.tables), 1)

If X is false, it returns 1 and nothing happens (there are other SQL errors below but not important). However if X is true, then the SELECT subquery is executed, returns more than one row and MySQL doesn't know how to fit the result in the original query so just replies the following MySQL error:
Subquery returns more than 1 row

We have our blind SQL injection turned into an exploitable SQL injection with an identifiable true/false answer.

From SQL injection to local file disclosure

We had to find a flag located in a file. MySQL conveniently provides load_file() and hopefully it was enabled. We can read the file character by character thanks to MySQL substr(). Instead of obtaining every character (by a linear brute-force or more clever dichotomy), sh4ka had the brilliant idea to read the file bit by bit, combining substr(), ord(), bin(), lpad() and again substr() MySQL functions.

Since I had finished another challenge and was available for work, I quickly catch up on this one and started to build an exploit, while Gu1 was building another one. Two exploits are better than one!

Due to challenge issues I wasn't able to properly finish it and sent it by mail as an alpha version. CTF ended, I finished it on a local installation of phplist 2.10.12 and here is the final exploit.

The exploit takes in argument:
  • host[:port] of webserver
  • path to phplist installation
  • admin username
  • admin password
  • file to read
  • optional verbose parameter to show the progress

It first logs in onto the admin panel, then using the SQL injection calculates the file size by dichotomy and finally reads the file bit by bit.

$ echo -n "poc" > /var/www/file
$ python localhost /phplist admin phplist /var/www/file 1
[*] phplist 2.10.12 SQL injection, local file disclosure
[+] Login successfull
[*] Retrieving '/var/www/file'
[+] '/var/www/file' length: 3
[+] Got: 'p'
[+] Got: 'po'
[+] Got: 'poc'


  1. How is this useful if you need the admin password???

  2. You don't need the admin password, you just need to be authenticated as an admin (stealing the cookie is enough).

    Requiring an admin access certainly reduces the attack surface but it is still a serious issue for phplist (file or db disclosure, privilege escalation).