Alternate ways of accessing functions / classes

Discussions of secure PHP coding. Security in software is important, so don't be afraid to ask. And when answering: be anal. Nitpick. No security vulnerability is too small.

Moderator: General Moderators

Post Reply
User avatar
Verminox
Forum Contributor
Posts: 101
Joined: Sun May 07, 2006 5:19 am

Alternate ways of accessing functions / classes

Post by Verminox »

I'm trying to write a sandbox-like script that analyses PHP code, checks all the function and method calls too see if any of them are restricted, and then evaluates the code. My approach to this is using the tokenizer extension and analysing the tokens to figure out which tokens are function calls, method calls, etc. and then check with an array of restricted functions/classes before proceeding.

Now, here are the cases that I have considered till now (bold identifiers are the tokens in consideration):

Accessing a class:

Code: Select all

class SubClass extends [b]MyClass[/b]
T_EXTENDS T_WHITESPACE T_STRING

Code: Select all

new [b]MyClass[/b]()
T_NEW T_WHITESPACE T_STRING

Code: Select all

[b]MyClass[/b]::staticMethod()
T_STRING [T_WHITESPACE] T_DOUBLE_COLON
Accessing a global function:

Code: Select all

[b]someFunction[/b]()
[T_WHITESPACE] T_STRING [T_WHITESPACE] '('
//Only those that are not preceeded by T_FUNCTION || T_DOUBLE_COLON || T_OBJECT_OPERATOR
I am also dissallowing the use of variable function calls / class initializations [eg. $foo()].

Now considering all of the above, is there any other sneaky way by which a global function can be called or a class can be accessed (static function, new isntance, extending the class)?

I'm looking for alternative ways malicious users could use to get around my checking. More specifically, using language constructs. Stuff like call_user_func() or eval() will be checked.
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Re: Alternate ways of accessing functions / classes

Post by Mordred »

Forget it. It's easy to mess up, hard to do right, and you seem to lack the expertise, so I'd bet you'll do it wrong (nothing personal here, just common sense). Moreover, noone should need this. If someone does, he's doing something wrong.

Just on the second page here is this thread: viewtopic.php?f=34&t=76929

This guy did it wrong, tried to fix it, but failed (I'm not sure if he even understood that he failed), and there's a vulnerable function among the ones he allows. Also, most of the arguments that noone should ever allow anyone to eval code are universal, they stand in your case as well.

If you're writing this for your own amusement, it's okay, just don't use it in production, and don't release it without a big red warning sign. Play with fire, get burned.

Play with eval(), get pwned.
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Alternate ways of accessing functions / classes

Post by alex.barylski »

Not sure what it is your trying to do. Running code in a sandbox is already possible via the runkit extension:

http://ca.php.net/manual/en/ref.runkit.php

If you are trying to figure out a way to prevent user uploaded code from executing illegal functions...you can configure that in php.ini somewhat using the disable_functions and disable_classes safe mode directives.

http://ca.php.net/manual/en/features.sa ... .safe-mode

Trying to detemrine if code is going to cause security heaaches is like finding a needle in a haystack -- technically it's possible, realistically, you'd be better off spending your time tuning the above php.ini settings.

p.s-If you wish to look at code and check whether methods are private, etc...don't use tokenizer -- it's a ton of work. Use reflection unless you absolutely must use tokenizing.

Cheers :)
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Re: Alternate ways of accessing functions / classes

Post by Mordred »

Hockey wrote:don't use tokenizer -- it's a ton of work. Use reflection unless you absolutely must use tokenizing.
Bah, wake up, man. You can't use reflection unless you evaluate the code submitted by the user^H^H^H^H attacker.
User avatar
Verminox
Forum Contributor
Posts: 101
Joined: Sun May 07, 2006 5:19 am

Re: Alternate ways of accessing functions / classes

Post by Verminox »

Mordred wrote:Forget it. It's easy to mess up, hard to do right, and you seem to lack the expertise, so I'd bet you'll do it wrong (nothing personal here, just common sense). Moreover, noone should need this. If someone does, he's doing something wrong.
Hard, but not impossible. Theres no fun without trying. And expertise isn't to be gained by being afraid of a challenge. I'd rather try, fail and learn something new than give up and go to sleep.
Mordred wrote:Just on the second page here is this thread: viewtopic.php?f=34&t=76929

This guy did it wrong, tried to fix it, but failed (I'm not sure if he even understood that he failed), and there's a vulnerable function among the ones he allows. Also, most of the arguments that noone should ever allow anyone to eval code are universal, they stand in your case as well.
My only enemies are language constructs that themselves challenge the sandbox. Which functions/classes finally get lucky to be used in the sandbox is something that can be thought of later.

Mordred wrote:If you're writing this for your own amusement, it's okay, just don't use it in production, and don't release it without a big red warning sign. Play with fire, get burned.

Play with eval(), get pwned.
I'm aware that I can never be 100% sure of the security of the code, so it's always going to be without guarentees. Sigh.
Hockey wrote:Not sure what it is your trying to do. Running code in a sandbox is already possible via the runkit extension:

http://ca.php.net/manual/en/ref.runkit.php
As Mordred mentioned, to use Runkit the code must first be evaluated, which I don't want.
Hockey wrote:If you are trying to figure out a way to prevent user uploaded code from executing illegal functions...you can configure that in php.ini somewhat using the disable_functions and disable_classes safe mode directives.

http://ca.php.net/manual/en/features.sa ... .safe-mode
Hmmm... I'll look into this.

EDIT: The approach here is disabling functions and classes, while my approach is probably going to be start with nothing, and then allow only certain functions and classes. Although this approach can also be implemented in PHP Safe Mode, there are also other missing features such as whether or not to allow access to $_SESSION, or $_POST or other superglobals that have some significance. Besides, IMO, a simple API with extensible features would be preferable over messing with php.ini directives directly.


I'm giving it a try. Let tons of experts exploit the code like it has never been expolited before until it gets a thorough workout. If everything can be fixed, good enough. I'll post a prototype here soon.
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Re: Alternate ways of accessing functions / classes

Post by Mordred »

And expertise isn't to be gained by being afraid of a challenge. I'd rather try, fail and learn something new than give up and go to sleep.

:bow:

This is certainly the right spirit! I'm not saying I believe you'll succeed in the end, but you'll definitely learn a lot! Do read the code (and the thread here) for "evil eval" or whatever that other project was called - there's a couple of good ideas there as well.
My only enemies are language constructs that themselves challenge the sandbox. Which functions/classes finally get lucky to be used in the sandbox is something that can be thought of later.
Here you are wrong. There's more than one way to skin a cat, and more than one way to execute code. Filtering language constructs is not enough. You must also deal with information leaks (user gives you a script to print your setup object which contains database credentials) and resource exhaustion (mainly CPU and memory).
I'll post a prototype here soon.
Sure do, it's always fun breaking other people's code ;)
User avatar
Verminox
Forum Contributor
Posts: 101
Joined: Sun May 07, 2006 5:19 am

Re: Alternate ways of accessing functions / classes

Post by Verminox »

Prototype: 0.1.0

Sourceforge doesnt provide PHP5 hosting so I couldn't host this script. So heres the code.

Current Features:
- Starts from scratch (cant access any functions/classes)
- You can allow certain functions
- You can allow certain functions
- All user-defined functions and classes can be accessed

Known issues:
- Var restrictions arent set up yet.
- This means $GLOBALS['sandbox']->allowFunction() is possible, but don't do it now.
- The other project had measures for preventing shell execution as a built-in language construct, or something like that. I'm not aware of this feature of PHP so somebody please enlighten me.

PHPSandbox.php

Code: Select all

<?php
/**
 * @package PHPSandbox
 * @version 0.1.0
 * @author Verminox <verminox@gmail.com>
 */
 
 
error_reporting(E_ALL);
header("Content-type: text/plain");
 
function TestFunc(){ echo "Test Function\n"; }
class TestClass{ public static function testMethod(){ echo "Test Method\n"; } }
try {
    $sandbox = new PHPSandbox();
    $sandbox->allowFunction('var_dump');
    $sandbox->allowFunction('TestFunc');
    $sandbox->allowClass('TestClass');
    $sandbox->evaluate(file_get_contents('source.php'));
} 
catch (Exception $e)
{ 
    echo $e;
}
 
 
 
/**
 * The Sandbox Environment
 */
class PHPSandbox
{       
    /**
     * List of global functions accessible from the sandbox
     * @var array
     */
    protected $allowedFunctions = array();
    
    /**
     * List of classes accessible from the sandbox
     * @var array
     */
    protected $allowedClasses = array();
    
    /**
     * Default constructor.
     */
    public function __construct()
    {
        // TODO
    }
    
    /**
     * Allow access to a global function from the sandbox
     *
     * @param string Name of the global function
     */
    public function allowFunction($function)
    {
        if ( !in_array( $function, $this->allowedFunctions ) )
        {
            $this->allowedFunctions[] = $function;
        }
    }
    
    /**
     * Whether a given function is allowed access from the sandbox
     *
     * @param string Name of the global function
     * @return bool
     */
    public function isAllowedFunction($function)
    {
        return in_array( $function, $this->allowedFunctions );
    }
    
    /**
     * Allow access to a class from the sandbox
     *
     * @param string Name of the class
     */
    public function allowClass($class)
    {
        if ( !in_array( $class, $this->allowedClasses ) )
        {
            $this->allowedClasses[] = $class;
        }
    }
    
    /**
     * Whether a given class is allowed access from the sandbox
     *
     * @param string Name of the class
     * @return bool
     */
    public function isAllowedClass($class)
    {
        return in_array( $class, $this->allowedClasses );
    }
    
    /**
     * Evaluate PHP Code in the sandbox.
     * 
     * The given PHP code will be evaluated within the confines of the sandbox. It will only be
     * able to access data that has been granted to the sandbox.
     * @param string The PHP source code to be executed (all PHP code must occur after opening a PHP tag)
     * @return mixed On success, whatever was returned in the source will be returned, or else NULL is returned.
     * If a parse-error occured then FALSE is returned.
     */
    public function evaluate($source)
    {
        // Get all tokens in the source
        $tokens = token_get_all($source);
        $token_manager = new PHPSandboxTokenManager($tokens);
        
        
        // Go through each token and check accessibility
        for($i=0;$i<$token_manager->size();$i++)
        {
            // Initialize current, previous and next tokens
            $current = $token_manager->getTokenAt($i);
            $previous = $token_manager->getTokenBefore($i);
            $next = $token_manager->getTokenAfter($i);
            
            // Handling tokens of type T_STRING
            if( $current[0] == T_STRING )
            {
                if( $previous[0] == T_CLASS )
                {
                    // Class Name: Definition
                    // Since the class is being defined within the source, it is hereby allowed
                    $this->allowClass($current[1]);
                }
                else if ( $previous[0] == T_FUNCTION )
                {
                    // Function Name: Definition
                    // Since the function is being defined within the source, it is hereby allowed
                    $this->allowFunction($current[1]);
                }
                else if ( $previous[0] == T_EXTENDS )
                {
                    // Class Name: Inheritence
                    if( $this->isAllowedClass($current[1]) )
                    {
                        continue;
                    }
                    else
                    {
                        throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
                    }
                }
                else if ( $previous[0] == T_NEW )
                {
                    // Class Name: New Object Initialization
                    if( $this->isAllowedClass($current[1]) )
                    {
                        continue;
                    }
                    else
                    {
                        throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
                    }
                }               
                else if ( $next[0] == T_CHARACTER && $next[1] == '(' )
                {
                    if ( $previous[0] == T_DOUBLE_COLON )
                    {
                        // Method Name: Static Method
                    } else if ($previous[0] == T_OBJECT_OPERATOR )
                    {
                        // Method Name: Non-static Method
                    } else
                    {
                        // Function Name: Global Function
                        if( $this->isAllowedFunction($current[1]) )
                        {
                            continue;
                        }
                        else
                        {
                            throw new PHPSandboxException("Use of restricted function '$current[1]'",$current[2]);
                        }
                    }
                }
                else if ( $next[0] == T_DOUBLE_COLON )
                {
                    // Class Name: Static Method
                    if( $this->isAllowedClass($current[1]) )
                    {
                        continue;
                    }
                    else
                    {
                        throw new PHPSandboxException("Use of restricted class '$current[1]'",$current[2]);
                    }
                } 
                else
                {
                    // Constant (NOTE: It could also be true or false or NULL)
                    // NEW NOTE: Actually, it could be anything else that we have not thought of
                }
            }
            // Make sure variable function names or class names are not used
            else if ( $current[0] == T_VARIABLE )
            {
                if ( $previous[0] == T_NEW )
                {
                    // Class Name: New Object Initialization
                    throw new PHPSandboxException("Restricted '$current[1]' - Variable class names not allowed",$current[2]);
                }
                else if ( $next[0] == T_CHARACTER && $next[1] == '(' )
                {
                    if ( $previous[0] == T_DOUBLE_COLON )
                    {
                        // Method Name: Static Method
                        throw new PHPSandboxException("Restricted '$current[1]' - Variable method names not allowed",$current[2]);
                    } else if ($previous[0] == T_OBJECT_OPERATOR )
                    {
                        // Method Name: Non-static Method
                        throw new PHPSandboxException("Restricted '$current[1]' - Variable method names not allowed",$current[2]);
                    } else
                    {
                        // Function Name: Global Function
                        throw new PHPSandboxException("Restricted '$current[1]' - Variable function names not allowed",$current[2]);
                    }
                }   
            }
            // Make sure variable variables are not allowed (i.e any standalone '$' is suspicious)
            else if ( $current[0] == T_CHARACTER && $current[1] == '$' )
            {
                // Standalone '$'
                throw new PHPSandboxException("Restricted '$'",$current[2]);
            }
            // Make sure eval() can never be called.
            else if ( $current[0] == T_EVAL )
            {
                // eval() attempted. Seriously... some nerve.
                throw new PHPSandboxException("Restricted call to eval()",$current[2]);
            }
            // Check for includes
            else if ( $current[0] == T_INCLUDE ||
                      $current[0] == T_INCLUDE_ONCE ||
                      $current[0] == T_REQUIRE ||
                      $current[0] == T_REQUIRE_ONCE )
            {
                // Including other files not allowed
                throw new PHPSandboxException("Attempt to include external file",$current[2]);
            }
        }
        
        // Now this is scary
        return eval('?>' . $source);
        
    }   
    
}
 
/**
 * Manages tokens acquired from the PHP Tokenizer extension
 */
class PHPSandboxTokenManager
{
    /**
     * Array of tokens
     * @var array
     */
    public $tokens = array();
    
    /**
     * Constructs a TokenManager 
     *
     * @param array A token array returned by token_get_all()
     */
    public function __construct($tokens)
    {
        // Convert $tokens array into pure array of arrays
        // For single characters, make it of type T_CHARACTER
        // Also add line numbers
        $line = 1;
        foreach($tokens as $token)
        {
            if(!is_array($token))
            {
                $this->tokens[] = array( T_CHARACTER, $token, $line );
            }
            else
            {
                if($token[0]==T_DOC_COMMENT){$token[0]=T_COMMENT;} // All comments are T_COMMENTs
                $this->tokens[] = array( $token[0], $token[1], $line );
                $line += ( substr_count($token[1],"\n") );
            }
        }
    }
    
    /**
     * Returns the number of tokens
     *
     * @return int Number of tokens
     */
    public function size()
    {
        return count($this->tokens);
    }
    
    /**
     * Returns the token at given index
     *
     * @param int offset of the token to retrieve
     * @return array The token if exists, or a token with both elements NULL otherwise
     */
    public function getTokenAt($i)
    {
        if( isset($this->tokens[$i]) )
        {
            return $this->tokens[$i];
        }
        else
        {
            return array(NULL,NULL);
        }
    }
    
    /**
     * Returns the next token
     * 
     * @param int offset of the current token
     * @param bool Whether to ignore whitespace
     * @return array The next token if exists, or a token with both elements NULL otherwise
     */
    public function getTokenAfter($i,$ignore_whitespace=true,$ignore_comments=true)
    {
        if( isset($this->tokens[$i+1]) )
        {
            if( $this->tokens[$i+1][0] == T_WHITESPACE && $ignore_whitespace==true )
            {
                return $this->getTokenAfter($i+1,$ignore_whitespace,$ignore_comments);
            }
            else if( $this->tokens[$i+1][0] == T_COMMENT && $ignore_comments==true)
            {
                return $this->getTokenAfter($i+1,$ignore_whitespace,$ignore_comments);
            }
            else
            {
                return $this->tokens[$i+1];
            }
        }
        else
        {
            return array(NULL,NULL);
        }
    }
        
    /**
     * Returns the previous token
     * 
     * @param int offset of the current token
     * @param bool Whether to ignore whitespace
     * @return array The previous token if exists, or a token with both elements NULL otherwise
     */
    public function getTokenBefore($i,$ignore_whitespace=true,$ignore_comments=true)
    {
        if( isset($this->tokens[$i-1]) )
        {
            if( $this->tokens[$i-1][0] == T_WHITESPACE && $ignore_whitespace==true )
            {
                return $this->getTokenBefore($i-1,$ignore_whitespace,$ignore_comments);
            }
            else if( $this->tokens[$i-1][0] == T_COMMENT && $ignore_comments==true)
            {
                return $this->getTokenBefore($i-1,$ignore_whitespace,$ignore_comments);
            }
            else
            {
                return $this->tokens[$i-1];
            }
        }
        else
        {
            return array(NULL,NULL);
        }
    }
}
 
 
 
/**
 * Exception thrown by the PHPSandbox evaluator
 */
class PHPSandboxException extends Exception
{
    /**
     * Line number of source that is being evaluated where the exception occurred.
     * @var int
     */
    protected $sourceLine;
    
    /**
     * Assigns $sourceLine and calls parent constructor
     */
    public function __construct($message, $line, $code=0)
    {
        $this->sourceLine = $line;
        parent::__construct($message, $code);
    }
    
    /**
     * Returns the line number of the source that is being evaluated where the exception occurred.
     * @return int The line number
     */
    public function getSourceLine()
    {
        return $this->sourceLine;
    }
    
    /**
     * Converts exception to string
     * @return string Exception message
     */
    public function __toString()
    {
        return "PHPSandboxException: " . $this->message . " on line " . $this->sourceLine;
    }
}
 
?>
source.php

Code: Select all

<?php
class MyClass //extends SuperClass
 
{
    public $member;
    
    static function staticMethod()
    {
        echo "Static Method\n";
    }
    
    function method()
    {
        echo "Method\n";
    }
}
 
function MyFunc()
{
    print "Global Function\n";
}
 
 
// This stuff works
TestFunc();
TestClass::testMethod();
MyFunc();
$obj = new MyClass();
$obj->member = array('Hello','World');
$obj->method();
MyClass::staticMethod();
var_dump($obj);
 
// This stuff shouldnt work
$var_MyFunc = 'MyFunc';
$var_MyClass = 'MyClass';
$var_method = 'method';
$var_staticMethod = 'staticMethod';
//$var_MyFunc();
//$obj = new $var_MyClass;
//$obj->$var_method( );
//MyClass::$var_staticMethod();
?>
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Alternate ways of accessing functions / classes

Post by alex.barylski »

I'm still confused as to what it is you are trying to accomplish. Are you trying to secure typical PHP scripts by running through your own custom sandbox environment? Disallowing certain functions, etc?

Are you letting anonymous people upload PHP code to your server? Are you trying to protect templates?

If it's the latter, just go with something like Smarty -- about the only time I would. :P
User avatar
Verminox
Forum Contributor
Posts: 101
Joined: Sun May 07, 2006 5:19 am

Re: Alternate ways of accessing functions / classes

Post by Verminox »

Hockey wrote:I'm still confused as to what it is you are trying to accomplish. Are you trying to secure typical PHP scripts by running through your own custom sandbox environment? Disallowing certain functions, etc?

Are you letting anonymous people upload PHP code to your server? Are you trying to protect templates?

If it's the latter, just go with something like Smarty -- about the only time I would. :P
Its actually the former. A method to evaluate untrusted PHP code without having to worry about getting your site vandalised. For example XHTML/CSS tutorial sites have a 'Try it yourself here' box, but this is risky with PHP. Templating can also find use for this class. Sure there is Smarty, but why go through the learning curve and restrict yourself to using only those features when you can have the power of PHP minus risky functions? I can think of several other uses for this functionality if it works right and is completely secure... but this is apparently the hard part.
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Re: Alternate ways of accessing functions / classes

Post by alex.barylski »

I think I have to side with Mordred on this one.

Practically, it doesn't serve much purpose. I mean, allowing users to upload custom PHP is asking for trouble...hackers are innovative that way...whatever you block, they'll figure out a way to unblock. Security is best handled using a principle of least privilege approach -- not principle of most privilege with attempts to safe gaurd everything.

Security always works best this way. For example, look at HTMLPurifier. It sanitizes using a whitelist, not a blacklist of known bad tags. This way, if you make a boo-boo the system is inherently(sp) more secure.

I think it's cool from an educational standpoint...but I don't think it would flourish beyond that. Certainly it will teach you a lot of how and why in terms of PHP security.

Cheers :)
Post Reply