---
# System prepended metadata

title: Project SEKAI CTF 2023 WRITE UP
tags: [CTF, Writeup]

---

# 1. Scanner Service 
![](https://hackmd.io/_uploads/r1Z0rRqan.png)
## Đề bài: 
- Challenge cho ta một trang web nơi ta có thể điền vào địa chỉ IP và trang web sẽ tiến hành thực thi quét địa chỉ id đó bằng command `nmap`:
![](https://hackmd.io/_uploads/SyGsUA9Tn.png)
- Challenge đồng thời cũng cho ta source và file docker, vì chall chỉ có instance 5 phút nên em sẽ build docker và thử nghiệm trên nó trước:
- Ta sẽ đi tìm hiểu source code của chall đã cấp cho ta trước đã:
    ![](https://hackmd.io/_uploads/Syp_PRqT3.png)
- Mình sẽ để ý vào 2 file `scanner.rb` và `scanner_helper.rb` vì nó là 2 file chính thực thi chức năng nmap của challenge, đồng thời cũng xem file Docker để xem flag sẽ được xử lý như thế nào:
- File **Dockerfile**:
```dockerfile!=
FROM ruby:2.7.5-alpine3.15

RUN apk add --update --no-cache supervisor

RUN adduser -D -u 1000 -g 1000 -s /bin/sh www

RUN mkdir /app
COPY src/ /app
COPY config/supervisord.conf /etc/supervisord.conf

COPY flag.txt /flag.txt

RUN mv /flag.txt /flag-$(head -n 1000 /dev/random | md5sum | head -c 32).txt

WORKDIR /app
RUN bundle install

RUN apk add nmap nmap-scripts --no-cache && rm -f /var/cache/apk/*

EXPOSE 1337

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
```
- flag của challenge đã bị biến đổi bằng cách thêm 32 ký tự random sau dấu `-`, làm cho việc tìm flag giờ chỉ còn cách là fuzzing hoặc RCE. (flag giờ là `flag-????????????????????????????????.txt`)
- File **scanner_help.rb**:
```ruby!=
def valid_port?(input)
  !input.nil? and (1..65535).cover?(input.to_i)
end

def valid_ip?(input)
  pattern = /\A((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})\z/
  !input.nil? and !!(input =~ pattern)
end

# chatgpt code :-)
def escape_shell_input(input_string)
  escaped_string = ''
  input_string.each_char do |c|
    case c
    when ' '
      escaped_string << '\\ '
    when '$'
      escaped_string << '\\$'
    when '`'
      escaped_string << '\\`'
    when '"'
      escaped_string << '\\"'
    when '\\'
      escaped_string << '\\\\'
    when '|'
      escaped_string << '\\|'
    when '&'
      escaped_string << '\\&'
    when ';'
      escaped_string << '\\;'
    when '<'
      escaped_string << '\\<'
    when '>'
      escaped_string << '\\>'
    when '('
      escaped_string << '\\('
    when ')'
      escaped_string << '\\)'
    when "'"
      escaped_string << '\\\''
    when "\n"
      escaped_string << '\\n'
    when "*"
      escaped_string << '\\*'
    else
      escaped_string << c
    end
  end
  escaped_string
end
```
- Nhìn ta cũng có thể thấy file này đóng vai trò filter đầu vào của người dùng, hàm `valid_port?` nó sẽ check port có nằm trong khoảng [1,65535] không. Hàm `valid_ip?` sẽ check địa chỉ IP bằng regex xem các phần của địa chỉ IP có chạy từ 0 đến 255, và cách nhau bằng 1 dấu `.` không. Rồi cuối cùng là check toàn bộ input bằng hàm `escape_shell_input`(ác mộng), hàm này filter gần như mọi thứ mà em có thế nghĩ ra khi muốn nhét shell vào như là: dấu cách, $, \n, *(gòi xong không `cat /flag*` được rồi :confounded: )
- File **scanner.rb**: file này sẽ là file chính tiến hành nmap địa chỉ của chúng ta:
```ruby!=
require 'sinatra/base'
require_relative '../helper/scanner_helper'

class ScanController < Sinatra::Base

  configure do
    set :views, "app/views"
    set :public_dir, "public"
  end

  get '/' do
    erb :'index'
  end

  post '/' do
    input_service = escape_shell_input(params[:service])
    hostname, port = input_service.split ':', 2
    begin
      if valid_ip? hostname and valid_port? port
        # Service up?
        s = TCPSocket.new(hostname, port.to_i)
        s.close
        # Assuming valid ip and port, this should be fine
        @scan_result = IO.popen("nmap -p #{port} #{hostname}").read
      else
        @scan_result = "Invalid input detected, aborting scan!"
      end
    rescue Errno::ECONNREFUSED
      @scan_result = "Connection refused on #{hostname}:#{port}"
    rescue => e
      @scan_result = e.message
    end

    erb :'index'
  end

end
```
- Nó include thằng `scanner_helper` vào. Khi ta post địa chỉ IP lên với body param là `services` thì đầu tiên cả input được filter bằng `escape_shell_input`, sau đó sẽ tiến hành tách địa chỉ IP vào 2 phần hostname và port phân tách nhau bởi dấu `:`, rồi check xem địa chỉ IP và port có hợp lệ không, rồi từ đó tiến hành câu lệnh nmap: `nmap -p port hostname` và trả về kết quả ở index.
## Phương hướng giải quyết
- Sau khi tìm hiểu source, ta biết chắc chắn là phải inject vào body param `services`, nhưng inject như nào thì ta chưa bíc :(. Thứ filter chính là hàm `escape_shell_input`, họ filter ` dấu cách , $, (, ), <, >, ;` nên các cách bypass dấu cách như: `cat</flag.txt, (cat,/flag.txt), cat${IFS}/flag.txt` đều hết cứu. Nhưng còn một thứ có thể cứu vãn được tình hình bây giờ, đó chính là dấu tab(`\t`) aka `%09`, sau khi thử với dấu tab này thì nó có thể bypass và thực thi được thành công nmap, nghĩa là em đã có thể inject vào được thằng port bằng dấu tab: (cảm ơn anh Tài vì đã chỉ cho em trick này :grin: )
![](https://hackmd.io/_uploads/ryOBTCqa3.png)
- Bypass được dấu cách, giờ ta phải quyết định đọc file bằng cách nào? RCE? Thực thi câu lệnh khác? Hay sử dụng chính nmap? Có khá nhiều hướng vào lúc này nên trong lúc cuộc thi diễn ra em đã không thể tìm được cách đúng, sau đó em được các anh hint là sử dụng chính nmap này để đọc file, nên giờ em sẽ đi theo hướng đó:
- Sau khi tìm hiểu về nmap và em đã thấy có một chức năng khá hay: `-iL <inputfilename>: Input from list of hosts/networks`, chức năng cho phép list ra số host và network, sau đó sử dụng tiếp chức năng: `-oN/-oX/-oS/-oG <file>: Output scan in normal, XML, s|<rIpt kIddi3,
     and Grepable format, respectively, to the given filename.` để hiện ra output mình có thể đọc được, kết hợp với việc sử dụng dấu tab. Với việc flag giấu cmn 32 ký tự đằng sau, em sử dụng dấu `?` để thể hiện sau file này còn có 32 ký tự mà em không biết nữa, vậy là em sẽ thử với payload là: `service=127.0.0.1:1337%09-iL%09/flag-????????????????????????????????.txt%09-`
![](https://hackmd.io/_uploads/SJenk1o6n.png)
- Có vẻ trang web đã thực hiện được việc nmap, nhưng nó lại không hiện ra :disappointed:, vì trang web không cho em ghi file trên local nên em cần phải trigger ra lỗi để chall đọc file này, vì thế em sẽ thêm `-` ở đằng sau: 
![](https://hackmd.io/_uploads/rJaZlkiph.png)
- Ngollll, em đã có được flag, giờ ốp cái payload: `127.0.0.1:1337%09-iL%09/flag-????????????????????????????????.txt%09-oN%09-` này là em có thể lấy được flag của challenge này rồi:
- Kết quả test ở instance:
![](https://hackmd.io/_uploads/r1SSMksTh.png)
- Ngoài ra trong quá trình tìm hiểu, em đã thấy có một người có một payload rất hay: `service=127.0.0.1:1337%09--script%09http-enum%09--script-args-file%09/flag-????????????????????????????????.txt%09-vvvvvv%09-dddddd`, với việc sử dụng switch script để tìm kiếm tài nguyên trên trang web bằng `http-enum`, sau đó truyền vào đường dẫn file flag ở `--script-args-file` để tiến hành đọc file, cuối cùng là đoạn `-vvvvvv` là verbose, nmap sẽ hiện ra nhiều thông tin hơn trong quá trình quét, còn `-dddddd` là debug, cũng để tăng mức độ chi tiết của output ở mức debug, kết quả sẽ như này:
![](https://hackmd.io/_uploads/S1H9b1oah.png)
- Nó đã có thể parse ra thành công được nội dung bên trong flag!! 
![](https://hackmd.io/_uploads/ry0mGJjpn.png)
- Ta cũng có thể bỏ chỗ `-v` và `-d` để nhìn đẹp hơn:
![](https://hackmd.io/_uploads/S1i-Gyi63.png)
- Flag: **SEKAI{4r6um3n7_1nj3c710n_70_rc3!!}**
