Featured image of post Câu chuyện cào điểm thi Hà Nội đầy gian nan

Câu chuyện cào điểm thi Hà Nội đầy gian nan

Ban đầu mình cũng không tính là đi cào điểm các bạn nhỏ về để nghiên cứu phổ điểm học sinh Việt Nam cho môn Xác suất thống kê hay gì mà chỉ đơn giản là được 1 đứa em gợi ý :3

Mình làm từ lúc đó mà lười viết nên giờ mới có bài cho các bạn đọc hix

Cổng thông tin tuyển sinh Sở giáo dục và đào tạo Hà Nội

Trang web mà mình sử dụng để thực hiện lấy dữ liệu điểm thi vào 10 của học sinh hà nội là trang Thông tin tuyển sinh - Cổng thông tin tuyển sinh mình cũng hơi rén với cái domain gov này nhưng mà cứ quất thôi, mình chỉ crawl thôi mà.

Bước đầu tiên để cào dữ liệu về là phải “tham quan” 1 vòng cái website này đã:

Trông khá đơn giản, không nhiều các thủ tục phức tạp –> có vẻ việc cào sẽ nhanh gọn

Tìm hướng để “móc” được dữ liệu từ trang web

Trước hết với cái website này thì mình cứ setup với Burpsuite trước đã

Mình thử với 1 SBD bất kì thuộc phạm vi học sinh của Hà Nội thì thấy

Trước hết thứ làm mình cảm thấy thu hút là cái getcapcha ở trong phần network khi mình tạo kết nối và gửi request SBD tới server, với kinh nghiệm với mấy cái encode này thì nó là 1 cái base64 (Sở encrypt mấy cái data đơn giản đến không ngờ - có thể là lười setup)

Weo giải mã cái base64 này ra thì được 1 file , 1 cái header PNG bình thường, có thể nó là cái capcha được refresh liên tục ở trang xem điểm vừa nãy nhỉ?

Và đúng như mình đoán, nó chỉ là cái capcha mỗi lần xem điểm, nên lúc này vô dụng mặc dù có thể khai thác được thứ gì đó từ nó :3 Đến tận lúc này mình mới nhận ra cái method của thằng capcha này là GET đúng là Reverser chơi web belike :)))

Tìm hiểu cách mã hóa và giải mã dữ liệu

Chuyển qua method POST, mình thấy ngay 1 thằng trả về ở /tra-cuu-diem-thi-10 và khi truy cập thì được 1 cái trang chứa raw điểm của học sinh mình vừa kiểm tra:

Thứ mình có thể khai thác hiện tại là cái key với "key":"aW5wdXREn2MXoaeanYXRhPTEz8XIeX2R8nOTAwMSdV1YtcCkmZ0eXBlQ2hlY2s9MDI=" trông khả nghi thật sự

Key này lại là base64 tiếp :33 decode được 1 đoạn thế này:

Chưa có gì khai thác được từ đây, mình cào tiếp tầm 5 bạn để so sánh thử các mã base64 này:

1
2
3
4
5
6
139001: aW5wdXREgBHx5LLwSYXRhPTEzDsqOznbvqOTAwMSh1D0qxcHiZ0eXBlQ2hlY2s9MDI=
139003: aW5wdXREpPO8lTzRvYXRhPTEzlFbd9Ew7BOTAwMy55FJ15M20Z0eXBlQ2hlY2s9MDI=
139004: aW5wdXRE6kzx1uRP7YXRhPTEziU5NcgmsqOTAwNCP3mx6tsdpZ0eXBlQ2hlY2s9MDI=
139009: aW5wdXREBWdLC6O8vYXRhPTEzFCnLjCEOOOTAwOSpNCPhpWEeZ0eXBlQ2hlY2s9MDI=
139008: aW5wdXREh1r9OiVkWYXRhPTEzpCJ3JpXAfOTAwOCsupK9zEsgZ0eXBlQ2hlY2s9MDI=
139007: aW5wdXRElmrWSo05SYXRhPTEzTPvVcPRLBOTAwNyAK4skQDOqZ0eXBlQ2hlY2s9MDI=

Ở đây mình thấy có các bộ được giữ khi thay đổi số báo danh là aW5wdXRE, YXRhPTEz, OTAw, Z0eXBlQ2hlY2s9MDI= tương ứng với inputD, ata=13, 900, và 1 đoạn khó hiểu cuối cùng gG—T6†V6³Ó$ hoặc hexa là 674797065436865636b3d30324 thì tạm thời mình bỏ qua

Thực ra thì khi gửi request liên tục bằng Burpsuite với 1 số báo danh duy nhất thì ngoại trừ 4 cái liệt kê bên trên ra thì còn lại cũng thay đổi liên tục theo (điều này khiến mình nghĩ rằng nó chẳng có nhiệm vụ nào khác mà chỉ được đưa vào để đánh lừa - thực chất sẽ lấy nhờ giá trị 4 cái ở trên kia) Nếu chú ý 1 chút:

  • ata=13 <–> YXRhPTEz tương ứng với 2 số đầu của SBD (tượng trưng cho mã trường)
  • 900 <–> OTAw tương ứng với số thứ tự và lấy thêm 2 ký tự sau sẽ được đẩy đủ 4 số còn lại (theo mã hóa base64)

Từ đây rút ra kết luận, thằng key này không chỉ là 1 cái base64 mà nó là nhiều cái base64 ghép lại với nhau bằng cách nối lại

Bây giờ mình sẽ tách cái base64 kia ra để tiện cho việc viết script cào dữ liệu:

Câu chuyện bây giờ trở nên khá dễ dàng (Với mỗi trường chỉ cần đổi lại mã trường 1 lần nhét vào query và chạy vòng lặp để cào từng học sinh trong mã trường đó)

Script Tạo key của từng trường học

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def generate_key_sequence():
    # Đoạn mở đầu
    prefix = "https://tsdaucap.hanoi.gov.vn/TraCuu/KetQuaTraCuuTuyenSinh10?key=aW5wdXREgBHx5LLwS"
    # Mã trường
    field_code = "YXRhPTEz"
    # Đoạn giữa
    middle_code = "DsqOznbvq"
    # Đoạn cuối
    suffix = "h1D0qxcHiZ0eXBlQ2hlY2s9MDI="

    # Duyệt qua các số từ 9001 đến 9999
    for i in range(9001, 10000):
        # Chuyển số sang base64 và loại bỏ hai dấu bằng cuối
        index_base64 = base64.b64encode(str(i).encode()).decode()[:-2]
        # Ghép các thành phần để tạo key
        key = f"{prefix}{field_code}{middle_code}{index_base64}{suffix}"
        print(key)
        
generate_key_sequence()

Và khi mình để ý thì bộ base64 được gen ra từ số thứ tự không khớp với bộ query của Sở GD&ĐT Hà Nội (Các ký tự cuối được tăng lên 2 giá trị)

Ví dụ 9001 –> Có thể là OTAwMQ, OTAwMR, OTAwMS thì bên sở lấy là cái thứ 3 nên mình sẽ phải sửa lại script 1 chút bằng cách tăng 2 giá trị cho char cuối cùng của base đó

1
2
3
4
# Chuyển số sang base64
index_base64 = base64.b64encode(str(i).encode()).decode()
# Tăng giá trị ký tự cuối cùng lên 2
modified_base64 = index_base64[:-3] + chr(ord(index_base64[-3]) + 2)

Và thế là mình có full bộ key của 1 trường :3. Nhưng mà chưa dừng lại ở đây được, điều mình muốn là hoàn toàn tự động, mình sẽ cần đến python và 1 vài thư viện như request, BeautifulSoup … để lấy data về và lọc chúng

Script lọc thông tin từng học sinh

  1. Thư viện mình dùng để lấy source raw của từng học sinh về là request
  2. Thư viện mình dùng để lọc các thông tin từ source raw là BeautifulSoup
  3. Thư viện mình dùng để lưu data lấy được vào file csv là csv

Ví dụ với 1 học sinh SBD là 139033 tương ứng với key=aW5wdXREgBHx5LLwSYXRhPTEzDsqOznbvqOTAzMyh1D0qxcHiZ0eXBlQ2hlY2s9MDI= thì mình thu được source khá là “clean”

source raw:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="row">
    <div class="col-md-2">
    </div>
    <div class="col-md-8 title-thong-tin-diem">
        Thông tin học sinh
    </div>
    <div class="col-md-2">
    </div>
</div>
<div class="row">
    <div class="card-body">
        <div class="form-group row">
            <div class="col-md-2">
            </div>
            <div class="col-md-8 box-thong-tin-diem">
                <div class="row" style="margin-bottom: 8px;">Số báo danh:&nbsp;  <b>139033</b></div>
                <div class="row" style="margin-bottom: 8px;">Mã học sinh:&nbsp;  <b>0150333732</b></div>
                <div class="row" style="margin-bottom: 8px;">Họ và tên:&nbsp;  <b>Nguyễn Hoàng Anh</b></div>
                <div class="row" style="margin-bottom: 8px;">Điểm thi:&nbsp;  <b>Ngữ văn: 8.50; Ngoại ngữ: 5.75; Toán:  9.00; Tổng điểm XT: 40.75</b></div>
            </div>
            <div class="col-md-2">
            </div>
        </div>
    </div>
</div>

Tư duy của mình ở đây sẽ chỉ là lọc các thông tin có trong thẻ <b> và tiến hành viết script luôn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import requests
from bs4 import BeautifulSoup

url = "https://tsdaucap.hanoi.gov.vn/TraCuu/KetQuaTraCuuTuyenSinh10?key=aW5wdXREgBHx5LLwSYXRhPTEzDsqOznbvqOTAzMyh1D0qxcHiZ0eXBlQ2hlY2s9MDI="

# Gửi yêu cầu HTTP và lấy về nội dung HTML
response = requests.get(url)
html_content = response.text

# Sử dụng BeautifulSoup để parse HTML
soup = BeautifulSoup(html_content, 'html.parser')

# Tìm tất cả các thẻ <b>
b_tags = soup.find_all('b')

# Lặp qua từng thẻ và in ra nội dung
for b_tag in b_tags:
    #Các thông tin cách nhau bởi dấu "," để tách ra khi sử dụng csv để lưu trữ
    print(b_tag.text.strip(), end =",")

Vậy là thu được những gì mình cần, ghép 2 đoạn script mình vừa code xong lại với nhau thì được code để in ra danh sách điểm thi học sinh 1 trường (với nhu cầu in nhiều trường hoặc cả Hà Nội thì thêm các vòng for lồng bên ngoài, thay đổi các ata của từng trường và bộ số thứ tự là xong)

Code hoàn chỉnh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import base64
from bs4 import BeautifulSoup
import requests

prefix = (
    "https://tsdaucap.hanoi.gov.vn/TraCuu/KetQuaTraCuuTuyenSinh10?key=aW5wdXREgBHx5LLwS"
)
field_code = "YXRhPTEz"
middle_code = "DsqOznbvq"
suffix = "h1D0qxcHiZ0eXBlQ2hlY2s9MDI="

for i in range(9001, 10000):
    try:
        index_base64 = base64.b64encode(str(i).encode()).decode()
        modified_base64 = index_base64[:-3] + chr(ord(index_base64[-3]) + 2)
        url = f"{prefix}{field_code}{middle_code}{modified_base64}{suffix}"
        response = requests.get(url)
        response.raise_for_status()

        html_content = response.text
        soup = BeautifulSoup(html_content, "html.parser")

        b_tags = soup.find_all("b")

        for b_tag in b_tags:
            #Các thông tin cách nhau bởi dấu "," để tách ra khi sử dụng csv để lưu trữ
            print(b_tag.text.strip(), end =",")
        print("\n")

    except requests.exceptions.RequestException as e:
        print(f"Error for index {i}: {e}")
    except Exception as e:
        print(f"An unexpected error occurred for index {i}: {e}")

Tổng kết

Tuy vẫn dựa trên cách cũ của mình ngày trước nhưng đối với website này của Sở giáo dục thì đã có phần khó hơn chút ở việc mã hóa các key khiến mình mất thêm thời gian đọc, thống kê và tìm ra quy luật giải mã các key này để chạy crawl dữ liệu điểm thi

Mong tương lai Sở mã hóa 1 cách khó hơn để thách thức các ae đi cào hơn :333. Tạm biệt!

Licensed By Spid3r