安全开发-web应用-PHP-留言板与登录防护
当你获得了目标的源码,这时候代码审计看不懂了怎么办,这时候就要学习到奇妙的web开发了,然后学会之后就可以转行去做开发了,当什么网安工程师(狗头)
对于我们而言,我们要学习的语言就是通常web开发中经常碰到的语言,比如PHP,JS,JAVA等等,当你拿到了源码却看不懂他是如何运行的,那真是非常操蛋了,所以这就是我们学习简单开发的原因。
那么首当其冲的当然是基本上是遇到最多的php了。
当然我们不要学习太深,毕竟不是真要去学习开发网站了,弄清楚重要模块的原理即可
PHP
这里我使用的是vscode+小皮面板+navicat搭建项目
功能:新闻列表,会员中心,资源下载,留言板,后台模块,模板引用,框架开发等等
技术:输入输出,超全局变量,数据库操作,逻辑架构,包含上传&下载删除
技术:JS&CSS混用,Cookie,Session操作,MVC架构,ThinkPHP引用等等
我相信看到这里的都是有点编程基础的,所以我就不叽里咕噜讲环境怎么装了
首先我们使用原生态的php来开发一个简单的留言板功能
首先我们要知道这个功能是怎么实现的呢
用户在浏览器中输入昵称还有提交的内容
服务器接收到数据然后写入数据库
数据库返回结果通知服务器
服务器将最终结果返回给浏览器
所以简单来说就是最基础的表单提交,还有数据库连接,这是php中最基础的知识点了
首先我们当然要准备一下数据库的结构了,创建数据库,表,还有里面的字段
首先创建一个数据库哈
正常连接即可,现在我们创建一下表
记录用户的用户名,提交的信息,ip地址和ua头
现在数据库创建成功了,我们就来搭建一下php服务
这里使用经典hello来测试服务是否正常启动,现在可以看到正常启用了,现在就来愉快的开发吧~~
//gbook.php <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Simple Guestbook</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .message { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; border-radius: 5px; } .message h4 { margin: 0 0 5px 0; } .message p { margin: 0; } </style> </head> <body> <h1>Guestbook</h1> <form id="form1" method="post" action=""> //post提交,action为空就是传参到自己 <label for="name">Name:</label><br> <input type="text" id="name" name="username" required><br><br> <label for="message">Message:</label><br> <textarea id="message" name="mesage" rows="5" required></textarea><br><br> <button type="submit">Submit</button> </form> //这里就开始写传参的php代码了 <?php ?> <h2>Messages</h2> <div id="messages"> <div class="message"> <h4>香波</h4> <p>测测测测</p> </div> <div class="message"> <h4>沸羊羊</h4> <p>测测测测</p> </div> </div> </body> </html>
首先写出大概的框架,功能就是用户在上面输入后然后成功提交之后就会在页面上显示出来,这只是个样式,还没有涉及到php连接数据库,我们继续来看普通的传参
//gbook.php <?php $u=$_POST['username']; //如果觉得打开报错很烦就这样@$_POST前面加一个@忽略错误 $m=$_POST['mesage']; //记得这里的名字最好和数据库里的字段同名,上面的表单也是同样的,这样就不怕乱乱七八糟 echo $u; echo $m; ?>
可以看到输入的信息通过传递给我定义的两个值之后,通过提交就会输出在页面上
既然现在可以正常传输了,现在就要连接数据库了,让他把接收到的数据保存到数据库中,这样就会一直存在了,而一般连接数据库我们要配置一个链接的文件config.php名字随意哈
这里为了演示就直接写在同一个地方了
//gbook.php <?php $dbip = 'localhost'; $dbuser = 'mikukuku123'; $dbpass = 'mikukuku123'; $dbname = 'demo01'; //必须有 $conn = mysqli_connect($dbip, $dbuser, $dbpass, $dbname);//定义一个变量接收值 if (!$conn) { die("连接错误: " . mysqli_connect_error()); }else { echo "连接成功"; } $u=@$_POST['username']; $m=@$_POST['mesage']; ?>
可以看到也是连接成功了,现在我们已经成功连接数据库,现在我们就可以对应的对相应字段传参
//gbook.php <?php $dbip = 'localhost'; $dbuser = 'mikukuku123'; $dbpass = 'mikukuku123'; $dbname = 'demo01'; $conn = mysqli_connect($dbip, $dbuser, $dbpass, $dbname); if (!$conn) { die("连接错误: " . mysqli_connect_error()); }else{ $u=@$_POST['username']; $m=@$_POST['mesage']; $i=@$_SERVER['REMOTE_ADDR'];//超全局变量,查看用户IP,具体自己搜 $ua=@$_SERVER['HTTP_USER_AGENT'];//同样,这个是UA头 $sql="INSERT INTO gbook (`username`, `mesage`, `ipaddr`, `uaget`) VALUES ('$u', '$m', '$i', '$ua')"; //这里的sql语句具体学习请看数据库应用,这里不多讲 mysqli_query($conn, $sql); //执行sql语句 if (mysqli_affected_rows($conn) > 0) { echo "<script>alert('留言成功!');</script>"; } else { echo "<script>alert('留言失败!');</script>"; } } ?>
现在说明sql语句执行成功了,我们去数据库看看
可以看到了我刚刚输入的东西还有后面偷偷记录的用户的IP地址和他的ua头,非常滴坏,但是可以看到他老是有空数据,这是因为我们对用户和输入没有进行判断,他空的他也会接收到ip地址和ua头,为了防止信息乱搞啊,我们还要进行对用户昵称和输入的判断,只有非空才进行写入
//gbook.php <?php $dbip = 'localhost'; $dbuser = 'mikukuku123'; $dbpass = 'mikukuku123'; $dbname = 'demo01'; $conn = mysqli_connect($dbip, $dbuser, $dbpass, $dbname); if (!$conn) { die("连接错误: " . mysqli_connect_error()); }else{ $u=@$_POST['username']; if (isset($u)){ //判断用户字段是否为空,否则不执行 $m=@$_POST['mesage']; $i=@$_SERVER['REMOTE_ADDR']; $ua=@$_SERVER['HTTP_USER_AGENT']; $sql="INSERT INTO gbook (`username`, `mesage`, `ipaddr`, `uaget`) VALUES ('$u', '$m', '$i', '$ua')"; mysqli_query($conn, $sql); if (mysqli_affected_rows($conn) > 0) { echo "<script>alert('留言成功!');</script>"; } else { echo "<script>alert('留言失败!');</script>"; } } } ?>
可以看到这样他就不记录非空信息了
现在既然我们可以存入数据库了,现在我们只需要让他显示在页面上即可
//gbook.php <?php $dbip = 'localhost'; $dbuser = 'mikukuku123'; $dbpass = 'mikukuku123'; $dbname = 'demo01'; $conn = mysqli_connect($dbip, $dbuser, $dbpass, $dbname); if (!$conn) { die("连接错误: " . mysqli_connect_error()); }else{ $u=@$_POST['username']; if (isset($u)){ $m=@$_POST['mesage']; $i=@$_SERVER['REMOTE_ADDR']; $ua=@$_SERVER['HTTP_USER_AGENT']; $sql="INSERT INTO gbook (`username`, `mesage`, `ipaddr`, `uaget`) VALUES ('$u', '$m', '$i', '$ua')"; mysqli_query($conn, $sql); if (mysqli_affected_rows($conn) > 0) { //逐行提取查询结果为关联数组 echo "<script>alert('留言成功!');</script>"; $sql1="SELECT * FROM gbook"; //查询数据库所有字段 $result = mysqli_query($conn, $sql1); //把数据库里的值给这个变量 while ($row = mysqli_fetch_assoc($result)) { echo "<div class='message'>"; echo "<h4>" . htmlspecialchars($row['username']) . "</h4>"; //防止恶意 HTML 注入(XSS),把用户输入的字符转义 echo "<p>" . htmlspecialchars($row['mesage']) . "</p>"; echo "</div>"; } } else { echo "<script>alert('留言失败!');</script>"; } } } ?>
可以看到已经是成功了
现在添加留言功能已经整完了,我们还需要一个管理留言的板块
我们在根目录下创建一个文件
用这个gbook-admin.php来管理留言
前面我们说了,一般连接数据库都是单独使用一个配置文件,我现在用的是内置的连接配置,这样的话如果我想增加这个管理功能又要写一遍连接配置,非常滴麻烦,所以我们现在用一个单独的配置文件config.php
//config.php <?php $dbip = 'localhost'; $dbuser = 'mikukuku123'; $dbpass = 'mikukuku123'; $dbname = 'demo01'; $conn = mysqli_connect($dbip, $dbuser, $dbpass, $dbname); ?>
准备好之后呢,我们只需要在网页中引用这个文件即可
//gbook-adnmin,php include '../config.php';
然后我们来看看是否工作正常,再gbook-admin.php中
//gbook-adnmin,php <?php include '../config.php'; $sql1="SELECT * FROM gbook"; $result = mysqli_query($conn, $sql1); while ($row = mysqli_fetch_assoc($result)) { echo "<div class='message'>"; echo "用户名"."<h4>" . htmlspecialchars($row['username']) . "</h4>"; echo "内容"."<p>" . htmlspecialchars($row['mesage']) . "</p>"; echo "IP地址"."<p>" . htmlspecialchars($row['ipaddr']) . "</p>"; echo "UA头"."<p>" . htmlspecialchars($row['uaget']) . "</p>"; echo "</div>"; } ?>
可以看到他能正常输入,说明我们的配置文件没有问题,还有前面gbook中的引用数据库,那里更改一下即可
现在我们继续来写删除功能,我们可以在每个留言中都添加一个删除按钮,这样就可以单独删除某个留言
//gbook-adnmin,php <?php include '../config.php'; // 检查是否有删除请求 if (isset($_GET['del'])) { $username = mysqli_real_escape_string($conn, $_GET['del']); // 获取要删除的用户名 $sql = "DELETE FROM gbook WHERE username = '$username'"; if (mysqli_query($conn, $sql)) { echo "留言已删除!<br>"; } else { echo "删除失败: " . mysqli_error($conn) . "<br>"; } } // 查询并显示所有留言 $sql1 = "SELECT * FROM gbook"; $result = mysqli_query($conn, $sql1); while ($row = mysqli_fetch_assoc($result)) { echo "用户名: " . htmlspecialchars($row['username']) . "<br>"; echo "内容: " . htmlspecialchars($row['mesage']) . "<br>"; echo "IP地址: " . htmlspecialchars($row['ipaddr']) . "<br>"; echo "UA头: " . htmlspecialchars($row['uaget']) . "<br>"; // 添加删除链接,传递留言的用户名 echo "<a href='gbook-admin.php?del=" . urlencode($row['username']) . "'>删除</a><br>"; echo "<hr>"; } ?>
比如我现在有两条数据
进入后台后可以看到这两条,我们现在来删除一个admin的数据
再去数据库里看看
可以看到只有一条记录了
通过这个就可以进行简单的删除操作
说起敲代码怎么能少了函数调用呢,我们定义一函数然后调用他也可以实现同样的效果
<?php include 'config.php'; function add_gbook($conn){ $u=@$_POST['username']; if(isset($u)){ $m=@$_POST['mesage']; $i=@$_SERVER['REMOTE_ADDR']; $ua=@$_SERVER['HTTP_USER_AGENT']; $sql="INSERT INTO gbook(`username`,`mesage`,`ipaddr`,`uaget`) VALUES ('$u','$m','$i','$ua')"; if(mysqli_query($conn,$sql)){ echo "<script>alert('留言成功!');</script>"; }else{ echo "<script>alert('留言失败!');</script> "; } } } function show_gbook($conn){ $sql1="SELECT * FROM gbook"; $result=mysqli_query($conn,$sql1); while($row=mysqli_fetch_row($result)){ echo "<hr>"; echo "用户名: " . htmlspecialchars($row[0]) . "<br>"; echo "内容: " . htmlspecialchars($row[1]) . "<br>"; echo "IP地址: " . htmlspecialchars($row[2]) . "<br>"; echo "UA头: " . htmlspecialchars($row[3]) . "<br>"; } } add_gbook($conn); // 添加留言 show_gbook($conn); // 显示留言 ?>
同样的,前面我们在写管理留言的时候也是直接写的,现在我们已经有了函数,可以直接访问文件然后调用那个函数
//gbook-adnmin,php include '../gbook.php'; show_gbook($conn); //直接在需要调用的地方改成这个即可
我们看看效果
可以看到也能正常的调用,成功力
这时候有人会问,主包主包,我删除键呢,由于我们前面调用的函数中没有写入删除按钮,所以我们还要再修改一下代码,让他成功调用
//gbook.php function show_gbook($conn,$del){ $sql1="SELECT * FROM gbook"; $result=mysqli_query($conn,$sql1); while($row=mysqli_fetch_row($result)){ echo "<hr>"; echo "用户名: " . htmlspecialchars($row[0]) . "<br>"; echo "内容: " . htmlspecialchars($row[1]) . "<br>"; echo "IP地址: " . htmlspecialchars($row[2]) . "<br>"; echo "UA头: " . htmlspecialchars($row[3]) . "<br>"; if ($del === 'del') { //定义del让他判断是否为del echo "<a href='gbook-admin.php?del=".htmlspecialchars($row[0])."'>删除</a>"; } } } add_gbook($conn); // 添加留言 show_gbook($conn, 'nonono'); // 显示留言
上面我们定义了一个del变量,然后判断这个变量的值,如果变量的值等于del的话,那么就会输出删除按钮,然后这里不需要删除按钮,所以调用的时候把del的值变成nonono,这样调用的时候就不会输出删除按钮了。
然后就是管理留言的页面了,很简单,只要在调用的时候给他一个正确的参数就可以正确调用了
//gbook-admin.php show_gbook($conn, 'del');
这样即可成功调用删除按钮,这时候又有人要问了,主包主包,为什么前面的html代码也显示了呀,这后台不应该显示呀,因为我们调用的时候并没有屏蔽掉前面的代码,所以该怎么办呢
最好的办法就是直接前后分离,把函数写在另外的单独php文件中,这样的话以后添加功能也只用修改一下这个函数文件即可,而且安全一点,所以我们现在把函数都迁移一下,创建一个gbook_function.php文件,然后copy即可哈,记得调用数据库文件
这就是目前的网站结构了,现在都访问一下看是否正常
可以看到前面没有删除按钮哈
后台删除按钮正常显示,这样基础的留言板功能现在差不多就完成了,很简单吧~
现在我们来扩展一下,一个留言或者评论区总有些秀儿想发点不一样的哈,比如一些表情,图片,这时候我们只有普通的文字评论区已经满足不了他们了
这时候我们就需要一些神奇的小插件了
UEditor
UEditor 是由百度「FEX前端研发团队」开发的所见即所得富文本web编辑器,这里就不多说了,具体去看官网
直接看他的使用,首先我们要引入他的js文件,在主页的html头里
//gbook.php <script src="/ueditor/ueditor.config.js"></script> //配置文件 <script src="/ueditor/ueditor.all.js"></script> //源码文件 <h1>Guestbook</h1> <form id="form1" method="post" action=""> <label for="name">Name:</label><br> <input type="text" id="name" name="username" required><br><br> <label for="message">Message:</label><br> <textarea id="message" name="mesage" rows="5" required></textarea><br><br> <script type="text/javascript"> UE.getEditor("message", { initialFrameWidth: 800, initialFrameHeight: 200, autoHeightEnabled: false, autoFloatEnabled: true, toolbars: [ ['fullscreen', 'source', '|', 'undo', 'redo', '|', 'bold', 'italic', 'underline', '|', 'insertimage'] ] }); </script> <button type="submit">Submit</button> </form>
在内容输入框下调用这个文本框,然后我这里给他设置了样式
initialFrameWidth: 800
:设置编辑器的初始宽度为 800 像素。initialFrameHeight: 200
:设置编辑器的初始高度为 200 像素。autoHeightEnabled: false
:禁用编辑器根据内容自动调整高度的功能。autoFloatEnabled: true
:启用工具栏浮动功能,当页面滚动时,工具栏会固定在顶部。toolbars: [...]
:定义编辑器的工具栏按钮。
在 toolbars
配置项中,指定了以下按钮:
fullscreen
:全屏编辑。source
:查看和编辑 HTML 源代码。undo
:撤销上一步操作。redo
:重做上一步操作。bold
:加粗文本。italic
:斜体文本。underline
:下划线文本。insertimage
:插入图片。
这样自定义一下输入框之后,可以看看效果
可以看到上面我自定义的东西就都正常显示了,然后我们随便发一个信息
可以看到我们虽然上传了图片,但是由于我们前面写的转义导致他直接输出路径了,没有实现我们想实现的效果,所以我们现在先暂时修改一下代码
把转义去掉
//gbook-funtion.php echo "内容: ". ($row[1]) . "<br>";
这里去掉之后,内容就能正常显示了,虽然有点危险
到这里这个留言板算是完成了,样式优化就懒得做了,这里只是演示功能实现,真香捏
这里就可以提一嘴关于前面我们说过的web漏洞的框架漏洞了,其实自己现在就已经可以判断了,你看我们编辑源码的时候前面内容我们把转义删掉之后
我们是不是就可以开始一些sql或者xss注入了呢,还有这个编辑器,这种来自第三方的框架或者插件我们也说过,只要网上说有历史漏洞,那我们就可以
直接利用历史漏洞就可以注入了,所以说这里其实有很多漏洞的嘞,不过现在只是演示开发,不演示漏洞怎么使用了,这个以后再提
其次我们现在来想一个问题,我们前面制作了一个gbook.php当做主页来显示留言板功能,然后又添加了一个admin来管理留言
这时候我们应该知道,像这种后台肯定是只有管理员才能操作呀,而现在由于我们并没有做这个登录功能,导致不管是谁来访问都是直接进入后台
那这样岂不是乱套了,这就是经典的未授权访问了,所以现在我们再来给这个网站再添加一个登录功能,来区分普通用户和管理员
我们先来认识一下,如何区别管理员和普通用户呢
首先web最基础的识别用户凭证的就是Cookie
先理解一下cookie的请求的过程
客户端向服务器发送HTTP请求。
服务器检查请求头中是否包含cookie信息
如果请求头中包含cookie信息,则服务器使用该cookie来识别客户端,否则服务器
将生成一个新的cookie。
服务器在响应头中设置cookie信息并将其发送回客户端。
客户端接收响应并将cookie保存在本地。
当客户端发送下一次HTTP请求时,它会将cookie信息附加到请求头中服务器收到请求并检查cookie的有效性。
如果cookie有效,则服务器响应请求。否则,服务器可能会要求客户端重新登录:
所以现在知道cookie的作用后,我们来去实现它
首先我们再管理文件夹中创建管理的php文件
admin-c.php //登录文件
index-c.php //登录成功首页文件
logout-c.php //登出文件
准备工作完成了,现在我们首先来写一下登录界面
//admin-c.php <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>明日方肘</title> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Roboto', sans-serif; } body { height: 100vh; background: linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%); display: flex; justify-content: center; align-items: center; } .login-container { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); width: 320px; } .login-container h2 { text-align: center; margin-bottom: 1.5rem; color: #333; } .login-container .input-group { margin-bottom: 1rem; } .login-container .input-group label { display: block; font-size: 0.9rem; color: #555; margin-bottom: 0.3rem; } .login-container .input-group input { width: 100%; padding: 0.6rem; font-size: 1rem; border: 1px solid #ccc; border-radius: 0.5rem; transition: border 0.3s; } .login-container .input-group input:focus { border-color: #66a6ff; outline: none; } .login-container button { width: 100%; padding: 0.8rem; background: #66a6ff; border: none; border-radius: 0.5rem; font-size: 1rem; color: white; cursor: pointer; transition: background 0.3s; } .login-container button:hover { background: #548cff; } </style> </head> <body> <div class="login-container"> <h2>泰拉</h2> <form action="#" method="post"> <div class="input-group"> <label for="username">你的名字</label> <input type="text" id="username" name="username" placeholder="请输入用户名" required> </div> <div class="input-group"> <label for="password">你的密码</label> <input type="password" id="password" name="password" placeholder="请输入密码" required> </div> <button type="submit">起飞</button> </form> </div> </body> </html>
一个基础的登录就写好了,首先我们理解一下这个工作的原理
首先是表单接收用户输入的账号密码,然后通过post传入后通过判断语句和数据库里的数据进行比对,如果成功了就生成cookie进行保存
然后跳转到对应的成功登录的页面,错误的话就提示用户密码错误
首先我们肯定是要接受到用户传输的数据,相信这个大家就不陌生了
$user=$_POST['username']; //创建一个变量接受用户输入的值,这里的名字是上面表单的名字 $pass=$_POST['password'];
然后就是对于判断用户的操作,这个肯定是对数据库进行操作,所以我们先来创建一个表来储存用户密码
表的结构简单的即可,我们首先对里面添加几个用户
比如经典的admin
这里的密码就不加密了,主要是做一个演示
然后我们在登录界面连接一下数据库,这个就不用多说了哈,接着我们就来对用户进行判断,首先我们构造一下sql语句,对应用户输入查询数据库里的数据
$sql = "SELECT * FROM `admin` WHERE `username` = '$user' AND `password` = '$pass';";
我们可以去数据库里试一下这个语句,看他返回什么
假如接收到用户的正确的输入,下面就返回正确的结果,如果是错误的返回的就是空,然后用if判断一下,就可以完成登录操作,首先我们看看前面有没有正确的输出
//amdin-c.php <?php include '../config.php'; $user = $_POST['username']; $pass = $_POST['password']; $sql = "SELECT * FROM `admin` WHERE `username` = '$user' AND `password`= '$pass';"; echo $sql; //调试语句 $result = mysqli_query($conn, $sql); ?>
可以看到能正常的接受到我们输入的值,那么我们现在再来加上判断语句
//admin-c.php if (mysqli_num_rows($result) > 0){ echo "ok"; }else{ echo "error"; }
可以看到输入正确的账号密码已经可以正确判断了,所以现在我们继续完善一下,当登录成功的时候弹窗一个登录成功,然后跳转到我们的后台页面中
//admin-c.php if (mysqli_num_rows($result) > 0){ echo '<script>alert("登录成功!");</script>'; //这个要不要无所谓,因为会直接跳转 header("Location: index-c.php"); }else{ echo '<script>alert("登录失败!");</script>'; }
当输入正确的值时,就会跳转到我们设定的页面,当然这里还啥都没有哈,同理错误就会弹窗失败
现在我们做完了基础的登录操作,现在就需要验证用户身份了,因为这时候的登录后页面还是可以直接访问
我们再创建一个用户,来做对照,随便创一个哈
现在我们来对cookie进行操作
if($_SERVER["REQUEST_METHOD"] == "POST"){ //这个是判断用户是否有登录操作,不然一直弹失败弹窗 if (mysqli_num_rows($result) > 0){ $expire = time() + 3600 * 24 * 30; // 设置cookie过期时间为30天 setcookie("username", $user, $expire, "/"); // 设置cookie,路径为根目录 setcookie("password", $pass, $expire, "/"); // 设置cookie,路径为根目录 echo '<script>alert("登录成功!");</script>'; header("Location: index-c.php"); exit(); }else{ echo '<script>alert("登录失败!");</script>'; } }
可以看到cookie值已经正常生成了
所以现在我们还要对cookie进行判断,我们先给后台首页进行简单的操作
//index-c.php <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>罗德岛</title> </head> <body> <h1>好久不见</h1> <p>欢迎您!<?php echo $_COOKIE['username']; ?></p> <p><a href="logout-c.php">您要走吗?</a></p> </body> </html>
这里让他显示登录后cookie中的用户名,这样就可以判断是谁在登录了。
可以看到由于cookie中记录了我们的用户名,登录后就会显示到我们写的语句上,我们试试另一个用户
可以看到cookie的部分作用就是这样的,记录用户的当前信息,我们换一个浏览器就知道了
由于我们前面没有登录操作,导致我们没有cookie值,然后这里就什么都没有了,这就是经典的未授权访问,因为我们还没有写对cookie值的验证,现在我们就来整一哈
//index-c.php <?php if($_COOKIE['username']=='admin'and $_COOKIE['password']=='123456'){ }else{ header('Location: admin-c.php'); } ?>
这里的判断本来是要引用数据库的,但是这里为了方便就直接写死了哈
/*这里小修改了一下,如果不是管理员登录也清除cookie,这样就不会乱套了*/
/*可以不用这么做,这只是我的突发奇想-继续看下面的即可*/
这里我就简单的进行了一下判断,如果cookie中不符合这个,那么就会被重定向到登录界面
比如我们用另一个test用户来试一下,不过我们要首先写一下登出的页面,因为他的cookie会一直保留,浏览器删了都没用我服了,写一下logout的页面
//logout-c.php <?php setcookie('username', '', time() - 3600, '/'); setcookie('password', '', time() - 3600, '/'); header('Location: admin-c.php'); exit(); ?>
然后用另一个用户测试一下
可以知道现在这个用户登录后进不去后台了
原因就是cookie不符合然后重定向了
我们可以抓包然后做一个简单的实验,要是我们发送的时候把admin的cookie记录了,我们是不是也可以直接访问后台呢
可以看到直接是进去了,所以说,保护我们的cookie也是非常重要的捏,只要在cookie的有效期内,你都可以直接是使用这个cookie访问到后台,非常的危险捏,因为他是储存在浏览器的捏,要是有小黑客在你电脑上留了个木马偷偷记录你的cookie,补豪,沃德密码,沃德合成玉呜呜呜呜呜
所以我们现在都用另一种验证方法
Session
他和cookie有什么不同呢
首先就是cookie是保存在客户端中,也就是浏览器,而session是保存在服务器中
还有前面说的cookie由于再客户端上,很容易被窃取,而seesion在服务器中就没那么容易了,但是说啊你都能窃取服务器的session了,还整啥登录不登录的,直接是服务器都日穿了,所以说这个用处不是很大,这两者的攻击目标都不同,一个是用户,一个是服务器,已经是两种不同的渗透思路了。
还有就是生命周期cookie他的生命周期是网站设置的,没有过期的话会一直保留,而session一般关闭浏览器之后就消失。
简而言之,这个session比cookie安全一点哈,但是两个各有优缺点,还是看实际项目的需求
所以我们也来整一下这个
首先创建关于session的文件
index-s.php //登录主页
admin-s.php //登录-采用session验证
logout-s.php //登出
首先我们copy一下登录代样式代码
然后主体的登录框架还是跟前面的一样,只是验证部分不太相同
//admin-s.php <?php include '../config.php'; $user = $_POST['username']; $pass = $_POST['password']; $sql = "SELECT * FROM `admin` WHERE `username` = '$user' AND `password` = '$pass';"; $result = mysqli_query($conn, $sql); if($_SERVER["REQUEST_METHOD"] == "POST"){ if (mysqli_num_rows($result) > 0){ session_start(); $_SESSION['username'] = $user; $_SESSION['password'] = $pass; header("Location: index-s.php"); exit(); }else{ echo '<script>alert("用户名或密码错误!");</script>'; } } ?>
这里需要提一下的是,这个seesion会话每次进行的时候都会在php.ini配置文件下里面的session路径下创建一个session文件,我看看我的是在哪里
可以看到路径指向的就是小皮的这个tmp路径下,我们现在登录一下试试
可以看到我刚刚登录了之后,他创建了一个文件,我们打开看一下
可以看到他记录了刚刚登录时候的信息,现在登录部分完成了,我们来同样对session值进行判断,主体框架还是跟上面同样,验证也很简单
//index-s.php <?php session_start(); if($_SESSION['username']=='admin'&& $_SESSION['password']=='123456'){ }else{ header('Location: admin-s.php'); exit(); } ?>
同样也是对比session中的值,然后对应跳转
可以看到也是实现了同样的效果
可以看到这个session对应到相应的文件中了,这就是当前用户登录的凭证
我们可以再实验一下,用另一个浏览器访问一下这个后台看看是什么效果
可以看到他也生成了一个session文件,我们去看看
显而易见,这个文件都是0kb了,说明里面什么都没有,这就给你防住了,简单的session用户验证不就完成了吗
这时候我们还可以思考一下,假如你已经拿下了服务器权限,找到了session文件,然后看到了这两个用户对应的session值,我们知道,下面那个是成功登录的admin用户,上面那个是什么都没有的,但是我们验证的时候并不是验证里面的内容,他验证的是文件的名字
这时候大聪明就知道了,我要是把成功登录的名字改到上面没成功的会发生甚么事呢
修改完我们再访问一下后台看看
蛙趣,又给我进去了
然后我们要是关掉浏览器或者别的操作导致这个session失效之后,也就是服务器没有这个session文件之后
可以看到重定向了一次,说明没有权限再去访问后台了
我们再来写一下登出功能,也就是说登出后就关闭session会话
//logout-s.php <?php session_start(); session_unset(); //释放当前变量 session_destroy(); //销毁会话中所有数据 header('Location: admin-s.php'); exit(); ?>
然后我们来看看效果
这是当前的session值
服务器中也对应生成
我们再点击一下退出
可以看到对应的session文件直接是没有了,这样就完成了一次完整的session会话过程,棒棒的捏
关于session验证的问题差不多也完成了,我们来看看最后一个技术
Token
Token 是一种轻量级、可携带的认证信息,常用于身份校验、防攻击、权限管理的技术
在用户登录后,服务器生成一个 Token(如 JWT),发给客户端,客户端每次请求时带上这个 Token,服务器通过验证 Token 知道是谁在访问。
这个token最主要的功能就是防止别人暴力破解登录的,每一次登录都会生成一个唯一的token值来对应,只有都对应上才能登录进去,而token是无法预测的,大大降低了爆破的成功率
我们来创建两个文件来使用token技术
token.php //生成token
token-check.php //检验token
首先我们来看看他的简单实现的效果
//token.php <?php session_start(); $token = bin2hex(random_bytes(16)); echo $token; ?>
random_bytes(16)
生成 16 个强加密级别的随机字节(共 128 位)。
bin2hex(...)
把字节转为 32 位的十六进制字符串(可输出、可存储)。
这样就成功了,然后每次刷新后都会刷新这个token值
前端代码还是使用之前的哈
稍微修改一下
//token.php div class="login-container"> <h2>泰拉</h2> <form action="token-check.php" method="post"> //将值传入这个文件用来判断 <input type="hidden" name="token" value="<?php echo $token; ?>"> //隐藏的提交一个token <div class="input-group"> <label for="username">你的名字</label> <input type="text" id="username" name="username" placeholder="请输入用户名" required> </div> <div class="input-group"> <label for="password">你的密码</label> <input type="password" id="password" name="password" placeholder="请输入密码" required> </div> <button type="submit">起飞</button> </form> </div>
然后将token与前面两个相结合
//token.php session_start(); if (!isset($_SESSION['token'])) { //判断token是否为空,是就生成token $_SESSION['token'] = bin2hex(random_bytes(16)); } // 不管是否新生成的,都从 session 中拿出来用 $token = $_SESSION['token']; setcookie('token', $token, time() + 3600, '/'); ?>
有了token后我们再写判断
//token-check.php <?php session_start(); if ( isset($_POST['token'], $_SESSION['token']) && hash_equals($_SESSION['token'], $_POST['token']) && $_POST['username'] === 'admin' && $_POST['password'] === '123456' ) { echo '<script>alert("登录成功");</script>'; // 登录成功,刷新 token,防止重放攻击 $_SESSION['token'] = bin2hex(random_bytes(16)); } else { echo '<script>alert("登录失败");</script>'; $_SESSION['token'] = bin2hex(random_bytes(16)); header('Location: token.php'); exit(); } ?>
最后发现写的有点问题哈,所以我最后选择了保留当前token,刷新并不会让当前token失效,然后我们登录看看
首先看到当前的token值,然后登录一下看看
可以看对应值都相等之后,他就登录成功了
这个token功能还不算完善,等我学号了再来修复吧我服了,改了一下午了,反正差不多就这样,token值来验证用户当前登录状态。
所以这上面代码的原理就是,访问登录页面后就生成一个token值,然后根据这个token值和账号密码一起判断登录
然后不管成功还是失败都会重置token值。实现简单的token防爆破,我们来演示一下
首先我们查看一下当前的token值
然后我们进行登录操作并抓包
这里不知道账号密码的情况下我们随便输入一个,然后抓住之后我们可以看到token值和输入的账号密码
然后这里为了简便我们只爆破密码部分,然后ctrl+i发送到爆破模块
我们给密码添加标签然后去爆破区域
输入一些乱七八遭的密码和正确的密码,我们来爆破一下看看,为了调试这里就不重定向了,稍微修改了一下代码,这就不多说了,太基础了
可以看到就算有正确的账号密码也是登录失败,因为代码中登录错误的时候token值已经被刷新了,导致这个旧的token值无法使用。
所以说这个token防止爆破还是很好的,但是如果攻击者知道了你这个token的工作原理之后,可能就会失效了,不过还是不妨碍他是一个防止爆破的手段。
所以说最后,三个关于web安全的已经全部写完了,总的来说只是防止爆破或者越权攻击的,所以说没有问题~~
未完待续
补充说明
前面我说了我的代码出了点问题,虽然说功能实现的差不多了,但是登录逻辑还是有点问题,现在修改好之后,找到了问题所在,原来是谷歌浏览器装的一些插件会重新访问网页,导致token值一直被刷新,现在看看修改完之后的
//token.php <?php // 生成Token并将其存储在Session中 session_start(); //1.因为是用的session维持会话,token已经绑定到下面的表单了 //2.token,生成之后直接存到session里,主要是方便重置token, //每次token随表单提交后都需要重置以保持token的唯一性。 $_SESSION['token'] = bin2hex(random_bytes(16)); ?>
//token-check.php <?php session_start(); $token = $_POST['token'] ?? ''; if ($token !== $_SESSION['token']) { // token不匹配,禁止访问 header('HTTP/1.1 403 Forbidden'); $_SESSION['token'] = bin2hex(random_bytes(16)); echo 'Access denied'; exit; }else{ $_SESSION['token'] = bin2hex(random_bytes(16)); if($_POST['username']=='admin' && $_POST['password']=='123456'){ echo '登录成功!'; echo '你是管理员可以访问文件管理页面!'; }else{ echo '登录失败!'; } } ?>
上面这两个代码的作用就是首先打开页面后自动生成token值并且记录到session中
然后登录失败成功之后都会刷新token
可以看到登录成功之后token值就被改变了,如果此时再刷新
在判断token值与服务器的session中的token已经不对应了,所以会直接显示拒绝连接,而不是登录失败,这个实验已经算是成功解决了~完美~~~