Page 1 of 1

Hole in my script? (I was hacked - need help finding hole!)

Posted: Thu Jul 19, 2007 7:33 pm
by plympton
Hello,

I was hacked the last 2 days by some Romanian scum - the first night they installed a trojan SSHD and captured my root logins (just found that out today - they used a /dev/saux patched SSH). The second night they installed an eBay / PayPal trojan. Fortunately I shut everything down hard before it became active, but I feel like crap about the whole thing. I've shut down the server (Fedora Core 4 based), and moved to a hosted scheme - it's just not worth the time/effort to run my own server anymore!

Anyway, seeing how I'm going to move the scripts I was using, I want to patch the hole he used to get in originally. I was running PHP 5 (latest) and Apache 2.0.54. He covered his tracks pretty well (edited .bash_history), but I found that he liked this script: login.php.

Potential problems: Could httpd be running as root? I executed apachectl as root, but my conf was nobody:nobody
Sessions: Is there a way to hijack a session and get in as root?
Permissions: My servers' permissions could have been wonky, and that the dir where the script was could have been nobody writable

If anyone has tips, I'd love to hear them - I don't want to redeploy my app before this gets solved (the login part I can live without - I just want to know the root cause of the breach!)

Thanks!!
-Dan


Here it is:

Code: Select all

<html>
<head>
<title>Login</title>
</head>
<body>
<?
   include ("gumgums_config.php");

   echo "Log-on To Plymptonia.com <br><br>";
   if ($Error != "")
   {
      echo "<b>Error: $Error</b><br>";
   }
   # echo "Current Page: $SCRIPT_NAME";
   # $tmp = $HTTP_ENV_VARS["HTTP_REFERER"];
   # echo "Other Referrer: $pagename";
?>

<form method="POST" action="<? echo HOMEDIR;?>loginverify.php">

  <p>Login: <input type="text" name="Login" size="20"></p>
  <p>Password: <input type="password" name="Password" size="20"></p>
  <p><input type="hidden" value="<?echo "$pagename"?>" name="pagename"></p>
  <p><input type="checkbox" name="NewUser" value="True">Create New User Account with The Above Login & Password</p>
  <p><input type="submit" value="Submit" name="B1"><input type="reset" value="Reset" name="B2"></p>
</form>

<?
/*
 <form method="POST" action="<?if ($pagename=="") {echo "/index.html"; } else {echo "$pagename";} ?>">
  <p><input type="submit" value="Cancel - Return to last page" name="B3">
 </form>
*/
?>

<form method="POST" action="/index.html">
  <p><input type="submit" value="Cancel - Return to home" name="B4">
</form>
&nbsp;
<p>
<p>
<script language=javascript>document.forms[0].Login.focus()</script>
</BODY>
</html>

Posted: Thu Jul 19, 2007 11:39 pm
by feyd
All of the variables used in the posted page are not defined in it. Where are they defined?

Posted: Thu Jul 19, 2007 11:57 pm
by plympton
feyd wrote:All of the variables used in the posted page are not defined in it. Where are they defined?
HOMEDIR is a constant defined in the config file included above, and $pagename, I believe, is passed in from the calling page - though if it's not, it would be undefined, I suppose. Would that pose a problem?

Loginverify.php calls login.php if the login fails (and passes in $Error, hence the "if ($Error != " ")

Those on the only 3 variables I can see in the whole script - everything else is HTML or commented out.

-Dan

Here is loginverify.php:

Code: Select all

<?
#
# Declare all session variables here?
#
include ("gumgums_config.php");

session_register("Authorized");
$Authorized = "False";
session_register("CanRead");
$CanRead = "False";
session_register("CanWrite");
$CanWrite = "False";
session_register("CanEdit");
$CanEdit = "False";
session_register("CanDelete");
$CanDelete = "False";
session_register("Username");
//$Username = "";
session_register("UserDir");
$UserDir = "/img";

?>

<html>
<head>
<title>Verify Login</title>
<META HTTP-EQUIV="REFRESH" CONTENT="15; URL=<?if ($pagename=="") {echo "/index.html"; } else {echo "$pagename";} ?>">
</head>

<body>

<?

#
# Open a  connection to the database
#
function openDB2($dbName,$dbLocation, $dbUsername, $dbPassword)
{
   $dbh = mysql_connect($dbLocation, $dbUsername, $dbPassword)
      or die ("Unable to connect to database.");
   mysql_select_db($dbName, $dbh)
      or die ("Unable to select database.");
   return $dbh;
}

#
# Close the Database Down
#
function closeDB2($dbh)
{
   mysql_close($dbh);
}

function logUser($table, $message, $dbh)
{
        $dateTime = date("l dS of F Y h:i:s A");
        $IPaddress = getenv("REMOTE_ADDR");
        $sessionID = session_id();
        $OS = getenv("HTTP_UA_OS");
        $browserType = getenv("HTTP_USER_AGENT");
        # echo "Login: $Login";
        switch ($table)
       {
       case "imageusers":
           $userSQL = "UPDATE imageusers SET lastdate='$dateTime', IPaddress='$IPaddress', OS='$OS', browserType='$browserType', sessionID='$sessionID' WHERE username='$message'";
           break;
       case "hackers":
           $userSQL = "INSERT into hackers (IPaddress, borndate, browserType, sessionID, OS, failure) VALUES ('$IPaddress', '$dateTime', '$browserType', '$sessionID', '$OS', '$message')";
           break;
       }
        # echo "userSQL: $userSQL<br>";
        $result = mysql_query($userSQL, $dbh)
           or die ("Can't update the database");
}


function authorizeUser($Login, $Password ,$dbh)
{
   switch($Login)
      {
         case "":
            return "False";
            break;
         default:
            $userSQL = "SELECT * from imageusers WHERE username='$Login'";
            # echo "userSQL: $userSQL";
            $result = mysql_query($userSQL, $dbh)
               or die("Bad query: ".mysql_error());
            # echo "result: $result";
            $numrows = mysql_numrows($result); // , $dbh);
                if ($numrows == 0)
            {
               # echo "That account doesn't exist<br>";
               return "False";
               break;
            } else {
               $DBLogin = (mysql_result($result,0,"username"));
               # echo "DBLogin: $DBLogin<br>";
               $DBPassword = (mysql_result($result,0,"password"));
               # echo "DBPassword: $DBPassword<br>";
               if ($Login == $DBLogin and $Password == $DBPassword)
               {
                  # echo "You are Authorized<br>";
                  logUser ("imageusers", $Login, $dbh);
                  $dateTime = date("l dS of F Y h:i:s A");
                  $IPaddress = getenv("REMOTE_ADDR");
                  $sessionID = session_id();
                  $OS = getenv("HTTP_UA_OS");
                  $browserType = getenv("HTTP_USER_AGENT");
                  $userSQL = "UPDATE imageusers SET lastdate='$dateTime', IPaddress='$IPaddress', OS='$OS', browserType='$browserType', sessionID='$sessionID' WHERE username='$Login'";
                  # echo "userSQL: $userSQL<br>";
                  $result = mysql_query($userSQL, $dbh)
                     or die ("Can't update the database");
                  $tmp = "True";
               } else {
                  $failure = "Login ($Login) or Password ($Password) incorrect";
                  logUser ("hackers", $failure, $dbh);
                  # echo "You are NOT Authorized<br>";
                  $tmp = "False";
               }
            }
            return $tmp;

       }
}

function authorizeVar($Login, $varField, $dbh)
{
   switch($Login)
      {
         case "":
            return "False";
            break;
         default:
            $userSQL = "SELECT * from imageusers WHERE username='$Login'";
            # echo "userSQL: $userSQL";
            $result = mysql_query($userSQL, $dbh)
               or die("Bad query: ".mysql_error());
            # echo "result: $result";
            $numrows = mysql_numrows($result); // , $dbh);
                if ($numrows == 0)
            {
               # echo "That account doesn't exist<br>";
               return "False";
               break;
            } else {
               return (mysql_result($result,0,"$varField"));
            }
      }
}

function testName ($name)
{
   trim($name);
   if (ucfirst($name) == "False" || ucfirst($name) == "True")
   {
      echo "<b>Login or Password can't be '$name', choose again</b><br>";
      return "False";
   }
   $tmp = strlen($name);
   if ($tmp > 20 or $tmp < 4)
      {
         echo "Login or Password has to be between 4 and 20 characters long.  Yours was $tmp characters long.<br>";
         return "False";
      }
   # echo "String: $name<br>";
   $NewName = ereg_replace ("[^0-9a-zA-Z]","",$name);
   # echo "NewString: $NewName<br>";
   if ($name != $NewName)
      {
         echo "<b>Can only have Characters and numbers in Login and Password</b><br>";
         return "False";
      }

   return $NewName;

}



function createNewUser($Login, $Password, $dbh)
{
   # echo "<b>Creating a new user account with the Login: $Login and Password: $Password</b><br>";
   $Login = testName($Login);
   if ($Login == "False")
   {
      # echo "<b>Failed Login test</b><br>";
      return "False";
   } else {
      # echo "Login: $Login<br>";
      # echo "Passed Login test<br>";
   }
   $Password = testName($Password);
   if ($Password == "False")
   {
      # echo "<b>Failed Password test</b><br>";
      return "False";
   } else {
      # echo "Password: $Password<br>";
      # echo "<b>Passed Password test</b><br>";
   }

   switch($Login)
      {
         case "":
            # echo "<b>Sorry, can't have a Zero Length Login</b><br>";
            return "False";
            break;
         default:
            $userSQL = "SELECT * from imageusers WHERE username='$Login'";
            # echo "userSQL: $userSQL";
            $result = mysql_query($userSQL, $dbh)
               or die("Bad query: ".mysql_error());
            # echo "result: $result";
            $numrows = mysql_numrows($result); // , $dbh);
            if ($numrows == 0)
            {
               # echo "That account doesn't exist (That's a good thing..)<br>";
               # return "False";
               $dateTime = date("l dS of F Y h:i:s A");
               $IPaddress = getenv("REMOTE_ADDR");
               $sessionID = session_id();
               $OS = getenv("HTTP_UA_OS");
               $browserType = getenv("HTTP_USER_AGENT");
               $userSQL = "INSERT into imageusers (username, password, borndate, lastdate, IPaddress, OS, browserType, sessionID, bornIPaddress, bornOS, bornbrowserType, bornsessionID) VALUES ('$Login', '$Password', '$dateTime', '$dateTime', '$IPaddress', '$OS', '$browserType', '$sessionID','$IPaddress', '$OS', '$browserType', '$sessionID')";
               # echo "userSQL: $userSQL<br>";
               $result = mysql_query($userSQL, $dbh)
                  or die ("Can't update the database");
            $userSQL = "SELECT * from imageusers WHERE username='$Login'";
            # echo "userSQL: $userSQL";
            $result = mysql_query($userSQL, $dbh)
               or die("Bad query: ".mysql_error());
            $DBLogin = (mysql_result($result,0,"username"));
            # echo "DBLogin: $DBLogin<br>";
            $DBPassword = (mysql_result($result,0,"password"));
            # echo "DBPassword: $DBPassword<br>";
            if ($Login == $DBLogin and $Password == $DBPassword)
               {
                  # echo "You are Authorized<br>";
                  $tmp = "True";
               } else {
                  # echo "You are NOT Authorized<br>";
                  $tmp = "False";
               }
            } else {
               # echo "That Login name already exists, choose another<br>";
               $tmp = "False";
            }
         }
         return $tmp;
}


#
#  Main starts here
#
   # echo "Login: $Login<br>";
   # echo "Password: $Password<br>";
   # echo "NewUser: $NewUser<br>";
   $dbh = openDB2('siterecords','localhost:3306', 'mysql', 'scrunchmail');
   if ($NewUser == "True")
   {
      $Authorized = createNewUser($Login, $Password, $dbh);
   } else {
      $Authorized = authorizeUser($Login, $Password, $dbh);
      # echo "Authorized: $Authorized<br>";
   }
   if ($Authorized == "True")
   {
      $Username = $Login;
      $CanRead = authorizeVar($Login, "canread", $dbh);
      $CanWrite = authorizeVar($Login, "canwrite", $dbh);
      $CanEdit = authorizeVar($Login, "canedit", $dbh);
      $CanDelete = authorizeVar($Login, "candelete", $dbh);
      $UserDir = authorizeVar($Login, "userdir", $dbh);
      # echo "Permissions: $CanRead $CanWrite $CanEdit $CanDelete $UserDir";
   ?>
          <SCRIPT LANGUAGE=JAVASCRIPT>
          window.location="<? if ($pagename=="") {echo "/index.html"; } else {echo "$pagename";} ?>"
          </SCRIPT>
          <NOSCRIPT>Your browser has disabled Javascript</NOSCRIPT>
   <?
   } else {
      $Error = "Wrong Login or Password, or Account already exists<br>";
      $Authorized = "False";
      $Username = "Guest";
      $UserDir = "/img";
      ?>
      <SCRIPT LANGUAGE=JAVASCRIPT>
          window.location="<? echo HOMEDIR;?>login.php?Error= <?echo $Error?>";
          </SCRIPT>
          <NOSCRIPT>Your browser has disabled Javascript</NOSCRIPT>
      <?
   }

   ?>
</html>

Posted: Fri Jul 20, 2007 12:14 am
by feyd
Was this code built from an example found online or in a book? The reason I ask is because it's designed around very old functions and concepts.

The few notable things:
  • session_register() has been on the deprecated list for quite some time. Use $_SESSION after session_start() has been called.
  • The use of short (open) tags (<?) denies future proofing of your code. Use long open tags, always: <?php
  • Your code appears to rely on register_globals being on. This "feature" has been turned off by default for some time due to poor security in scripts. The "feature" has been removed in future versions of PHP.
  • $dateTime in logUser() isn't using a standard date-time string format. Are you aware of this?
  • getenv() is used where $_SERVER provides most, if not all, of the values.
  • I don't understand the usage of strings for false and true are used where/when the actual boolean values would make more sense.
  • $pagename appears to allow a user to inject all sorts of goodies into your Javascript among other things.
  • ereg_replace() used instead of preg_replace(). Last I saw ereg is being removed in future versions of PHP.
  • $Login (in all its uses) appears to be the major culprit of injection.

Posted: Fri Jul 20, 2007 12:04 pm
by plympton
feyd wrote:Was this code built from an example found online or in a book? The reason I ask is because it's designed around very old functions and concepts.

The few notable things:
  • session_register() has been on the deprecated list for quite some time. Use $_SESSION after session_start() has been called.
  • The use of short (open) tags (<?) denies future proofing of your code. Use long open tags, always: <?php
  • Your code appears to rely on register_globals being on. This "feature" has been turned off by default for some time due to poor security in scripts. The "feature" has been removed in future versions of PHP.
  • $dateTime in logUser() isn't using a standard date-time string format. Are you aware of this?
  • getenv() is used where $_SERVER provides most, if not all, of the values.
  • I don't understand the usage of strings for false and true are used where/when the actual boolean values would make more sense.
  • $pagename appears to allow a user to inject all sorts of goodies into your Javascript among other things.
  • ereg_replace() used instead of preg_replace(). Last I saw ereg is being removed in future versions of PHP.
  • $Login (in all its uses) appears to be the major culprit of injection.
Well, I wrote it a few years ago, was probably pulled from some example off the web, and I'm a self-taught programmer, so there's no doubt it's pretty ugly!

Register_globals was still the rage back when I wrote it, and I didn't follow the exploits too carefully since then. Now I see how bad it's gotten.

Thanks for the tips - I'm still not sure how $Login could be used to inject code - would it be in the switch($Login) - if $Login was formatted as PHP code, would it be executed and evaluated? Could you post to the form $Login=<?exec("wget ssh_tarball.tgz");exec("tar -xzf ssh_tarball.tgz.... etc... ?> and it be evaluated in the switch statement? EGAD if so! Man oh man that would be bad....

If that's not what you were thinking - is there an example of how $Login could be manipulated? I really appreciate the advice!

As for the depricated stuff, I need to comb the code and take care of all that before going further. I did the site a few years ago, and just developed an interest in it again (it's a photo sharing website for me and my family), and I'm a much better programmer now.

Thanks!
-Dan

Posted: Fri Jul 20, 2007 5:30 pm
by feyd
$Login allows SQL injection.

Posted: Fri Jul 20, 2007 6:10 pm
by plympton
feyd wrote:$Login allows SQL injection.
Hmm.. that's fine and dandy - to be honest, I didn't really care about my database (it wasn't critical, just convienent). But what happened is that they executed ROOT LEVEL CODE! They got a command line, downloaded a hacked SSHD, installed it, keylogged my root password, and then had a field day. For some reason (based on the command history I dug up), "login.php" was very important to find - did they just want to poke around the database and see if it help CC information or something worth downloading?

So, my script can lead to SQL injection, which I'll take care of. I'm just wondering how they got in to the command line.. maybe I'm just overlooking that there probably was a username with a weak password... but there were only 2 users allowed to login to SSH at all... grr.....

This is driving me nuts. How did login.php lead to access to a bash shell.... ?

-Dan

Posted: Fri Jul 20, 2007 6:37 pm
by feyd
There are many ways of gaining access but unfortunately forum policy doesn't allow us to talk about how to compromise systems. It's hard to say what they used to gain access without knowing a lot more however. It could easily be a hole on the server, not necessarily your code.

Posted: Fri Jul 20, 2007 7:07 pm
by plympton
feyd wrote:There are many ways of gaining access but unfortunately forum policy doesn't allow us to talk about how to compromise systems. It's hard to say what they used to gain access without knowing a lot more however. It could easily be a hole on the server, not necessarily your code.
Aargh! How can I patch a hole against an attack if we can't discuss it! They clearly wanted login.php - there were 5-6 lines of shell requests using "whereis" "find" and the like. It was clearly a script they wanted to get their hands on. Now, if it was just to mess with my SQL database, fine, I can live with that. I can code around that. But if could lead to shell access, it makes me want to ditch PHP altogether.

I understand the desire not to teach rogues how to attack, but I'm sure the rootkit, underground, hacker forums are way better informed than any routine discussion here. Ugh.

So, the bottom line is I'll clean up my code the best I can, given my skills, post it to my new host who presumably has a better configured server than my old FC4 machine was, and hope for the best, because we can't talk about PHP security hole is a PHP - Security discussion forum.

Heck, I should just find a hacker forum, set up my server again as a honeypot, and invite people to hack away on it.

Sorry for the frustration, but that does seem rather bizarre. Security through obscurity is stupidity.

-Dan

Posted: Fri Jul 20, 2007 7:25 pm
by feyd
We can talk about security and security holes, but we can't discuss how to exploit those holes to do further damage. It's a liability issue we wish to not take on.

I don't have the luxury of time to read through and find all the holes in your code. Nor do I have the time to write long posts on them. Read through the volume of work put out by at shiflett.org and phpsec.org.

Posted: Tue Jul 24, 2007 9:22 am
by timgolding
These might help. These are the three major security holes that i come across on a regular basis. Here is some example documentation on those subjects.

PHP injections :
http://www.rohitab.com/discuss/lofivers ... t9626.html
answer: define your variables

SQL injections :
http://www.governmentsecurity.org/artic ... atters.php
answere: real escape any query strings

Session impersonation :
http://phpsec.org/projects/guide/4.html
Don't use shared servers or ask your host what provisions they have made against session impersination

Read this too:
http://www.w3.org/2001/tag/doc/whenToUseGet.html

I am no expert on security but i hope these help.

Posted: Tue Jul 24, 2007 3:32 pm
by plympton
timgolding wrote:These might help. These are the three major security holes that i come across on a regular basis. Here is some example documentation on those subjects.

PHP injections :
http://www.rohitab.com/discuss/lofivers ... t9626.html
answer: define your variables

SQL injections :
http://www.governmentsecurity.org/artic ... atters.php
answere: real escape any query strings

Session impersonation :
http://phpsec.org/projects/guide/4.html
Don't use shared servers or ask your host what provisions they have made against session impersination

Read this too:
http://www.w3.org/2001/tag/doc/whenToUseGet.html

I am no expert on security but i hope these help.
Thanks for the tips - that helps!

I think it turns out it was a red-herring, actually. While my script was by no means secure, I believe the attacker used a security hole in VNC 4.1.1 that allowed login without authentication. Since I had a root login available (another no-no), they could just waltz in an do whatever they wanted. I think they searched for login.php so that they could dig deeper and find any "valuable" data - of course there was none, but they don't know that.

Once I closed the VNC hole, the attacks stopped. Limiting SSH to 3 IP's in hosts.allow also reduced the number inquiries that way, too.

I'm suprised I hadn't heard of the VNC security hole, as it was a pretty big freakin' gaping one....

-Dan

Posted: Wed Jul 25, 2007 3:34 am
by timgolding
I had similar problems with VNC 4.1.1. Nowadays i prefer not to have root access, just a good hosting company who can look after everything for me :)

Posted: Thu Jul 26, 2007 6:59 am
by timvw
Imho, neither VNC or root access are required to manage a webserver... Configure your sshd to (only) use public/private key authentication, preferably with ip/host verification, and grant privileges to the users that need it via sudo...

And as already mentionned, ditching php isn't going to buy you very much, the problem is with incorrect managment of the server itself...