diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7da5a40..4da5e954 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,8 +53,9 @@ jobs: mkdir build cd src/extension phpize + cd ../lib + go build -buildmode=c-archive -o ../../build/libaikido_go.a cd ../../build - go build -buildmode=c-archive -o libaikido_go.a ../src/lib/aikido_lib.go CXX=g++ CXXFLAGS="-fPIC -I../include" LDFLAGS="-L./ -laikido_go" ../src/extension/configure --with-php-config=$HOME/php/bin/php-config make make install diff --git a/build.sh b/build.sh index 529295c8..045008ad 100755 --- a/build.sh +++ b/build.sh @@ -2,8 +2,9 @@ rm -rf build mkdir build cd src/extension phpize +cd ../lib +go build -buildmode=c-archive -o ../../build/libaikido_go.a cd ../../build -go build -buildmode=c-archive -o libaikido_go.a ../src/lib/aikido_lib.go -CXX=g++ CXXFLAGS="-fPIC -I../include" LDFLAGS="-L./ -laikido_go" ../src/extension/configure +CXX=g++ CXXFLAGS="-fPIC -g -O0 -I../include" LDFLAGS="-L./ -laikido_go" ../src/extension/configure make make install \ No newline at end of file diff --git a/src/extension/aikido.cpp b/src/extension/aikido.cpp index 19b1a2a6..4a508330 100644 --- a/src/extension/aikido.cpp +++ b/src/extension/aikido.cpp @@ -18,33 +18,51 @@ using namespace std; using json = nlohmann::json; -ZEND_NAMED_FUNCTION(handle_file_get_contents); ZEND_NAMED_FUNCTION(handle_curl_init); ZEND_NAMED_FUNCTION(handle_curl_setopt); +ZEND_NAMED_FUNCTION(handle_shell_execution); + struct FUNCTION_HANDLERS { zif_handler aikido_handler; zif_handler original_handler; }; -#define AIKIDO_REGISTER_HANDLER(function_name) { #function_name, { handle_##function_name, nullptr } } +/* + Macro for registering an Aikido handler in the HOOKED_FUNCTIONS map. + It takes as parameters the PHP function name to be hooked and C++ function + that should be called when that PHP function is executed. + The nullptr part is a placeholder where the original function handler from + the Zend framework will be stored at initialization when we run the hooking. +*/ +#define AIKIDO_REGISTER_HANDLER_EX(function_name, function_pointer) { std::string(#function_name), { function_pointer, nullptr } } + +/* + Shorthand version of AIKIDO_REGISTER_HANDLER_EX that constructs automatically the C++ function to be called. + For example, if function name is curl_init this macro will store { "curl_init", { handle_curl_init, nullptr } }. +*/ +#define AIKIDO_REGISTER_HANDLER(function_name) { std::string(#function_name), { handle_##function_name, nullptr } } -unordered_map HOOKED_FUNCTIONS = { - AIKIDO_REGISTER_HANDLER(file_get_contents), + +unordered_map HOOKED_FUNCTIONS = { AIKIDO_REGISTER_HANDLER(curl_init), - AIKIDO_REGISTER_HANDLER(curl_setopt) + AIKIDO_REGISTER_HANDLER(curl_setopt), + + AIKIDO_REGISTER_HANDLER_EX(exec, handle_shell_execution), + AIKIDO_REGISTER_HANDLER_EX(shell_exec, handle_shell_execution), + AIKIDO_REGISTER_HANDLER_EX(system, handle_shell_execution), + AIKIDO_REGISTER_HANDLER_EX(passthru, handle_shell_execution), + AIKIDO_REGISTER_HANDLER_EX(popen, handle_shell_execution), + AIKIDO_REGISTER_HANDLER_EX(proc_open, handle_shell_execution) }; -#define AIKIDO_HANDLER_START(function_name) php_printf("[AIKIDO-C++] Handler called for \"" #function_name "\"!\n"); -#define AIKIDO_HANDLER_END(function_name) HOOKED_FUNCTIONS[#function_name].original_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); +#define AIKIDO_GET_FUNCTION_NAME() (ZSTR_VAL(execute_data->func->common.function_name)) -ZEND_NAMED_FUNCTION(handle_file_get_contents) { - AIKIDO_HANDLER_START(file_get_contents); - AIKIDO_HANDLER_END(file_get_contents); -} +#define AIKIDO_HANDLER_START() php_printf("[AIKIDO-C++] Handler called for \"%s\"!\n", AIKIDO_GET_FUNCTION_NAME()); +#define AIKIDO_HANDLER_END() HOOKED_FUNCTIONS[AIKIDO_GET_FUNCTION_NAME()].original_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); ZEND_NAMED_FUNCTION(handle_curl_init) { - AIKIDO_HANDLER_START(curl_init); + AIKIDO_HANDLER_START(); zend_string *url = NULL; @@ -53,15 +71,15 @@ ZEND_NAMED_FUNCTION(handle_curl_init) { Z_PARAM_STR_OR_NULL(url) ZEND_PARSE_PARAMETERS_END(); - AIKIDO_HANDLER_END(curl_init); + AIKIDO_HANDLER_END(); if (Z_TYPE_P(return_value) != IS_FALSE) { // Z_OBJ_P(return_value) json curl_init_event = { - { "event", "function_hooked" }, + { "event", "function_executed" }, { "data", { { "function_name", "curl_init" }, - { "parameters", {} } + { "parameters", json::object() } } } }; if (url) { @@ -73,7 +91,7 @@ ZEND_NAMED_FUNCTION(handle_curl_init) { } ZEND_NAMED_FUNCTION(handle_curl_setopt) { - AIKIDO_HANDLER_START(curl_setopt); + AIKIDO_HANDLER_START(); zval *curlHandle = NULL; zend_long options = 0; @@ -92,11 +110,11 @@ ZEND_NAMED_FUNCTION(handle_curl_setopt) { std::string urlString(ZSTR_VAL(url)); json curl_setopt_event = { - { "event", "function_hooked" }, + { "event", "function_executed" }, { "data", { { "function_name", "curl_setopt" }, { "parameters", { - "url", urlString + { "url", urlString } } } } } }; @@ -106,7 +124,36 @@ ZEND_NAMED_FUNCTION(handle_curl_setopt) { zend_tmp_string_release(tmp_str); } - AIKIDO_HANDLER_END(curl_setopt); + AIKIDO_HANDLER_END(); +} + +ZEND_NAMED_FUNCTION(handle_shell_execution) { + AIKIDO_HANDLER_START(); + + zend_string *cmd = NULL; + + ZEND_PARSE_PARAMETERS_START(1,-1) + Z_PARAM_OPTIONAL + Z_PARAM_STR(cmd) + ZEND_PARSE_PARAMETERS_END(); + + std::string cmdString(ZSTR_VAL(cmd)); + + std::string functionNameString(AIKIDO_GET_FUNCTION_NAME()); + + json shell_execution_event = { + { "event", "function_executed" }, + { "data", { + { "function_name", functionNameString }, + { "parameters", { + { "cmd", cmdString } + } } + } } + }; + + GoOnEvent(shell_execution_event); + + AIKIDO_HANDLER_END(); } /* For compatibility with older PHP versions */ @@ -123,11 +170,11 @@ PHP_MINIT_FUNCTION(aikido) #endif for ( auto& it : HOOKED_FUNCTIONS ) { - zend_function* function_data = (zend_function*)zend_hash_str_find_ptr(CG(function_table), it.first, strlen(it.first)); + zend_function* function_data = (zend_function*)zend_hash_str_find_ptr(CG(function_table), it.first.c_str(), it.first.length()); if (function_data != NULL) { it.second.original_handler = function_data->internal_function.handler; function_data->internal_function.handler = it.second.aikido_handler; - php_printf("[AIKIDO-C++] Hooked function \"%s\" using aikido handler %p (original handler %p)!\n", it.first, it.second.aikido_handler, it.second.original_handler); + php_printf("[AIKIDO-C++] Hooked function \"%s\" using aikido handler %p (original handler %p)!\n", it.first.c_str(), it.second.aikido_handler, it.second.original_handler); } } diff --git a/src/lib/aikido_lib.go b/src/lib/aikido_lib.go deleted file mode 100644 index d9d8debc..00000000 --- a/src/lib/aikido_lib.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import "C" -import ( - "fmt" - "net/url" -) - -func NormalizeDomain(rawurl string) string { - parsedURL, err := url.Parse(rawurl) - if err != nil { - return "" - } - return parsedURL.Hostname() -} - -//export OnEvent -func OnEvent(event string) string { - fmt.Println("[AIKIDO-GO] OnEvent: ", event) - return "{}" -} - -func main() {} diff --git a/src/lib/go.mod b/src/lib/go.mod new file mode 100644 index 00000000..6967dfdf --- /dev/null +++ b/src/lib/go.mod @@ -0,0 +1,3 @@ +module main + +go 1.18 diff --git a/src/lib/handle_function_executed.go b/src/lib/handle_function_executed.go new file mode 100644 index 00000000..1e5df3b7 --- /dev/null +++ b/src/lib/handle_function_executed.go @@ -0,0 +1,24 @@ +package main + +type functionExecutedHandlersFn func(map[string]interface{}) string + +var functionExecutedHandlers = map[string]functionExecutedHandlersFn{ + "curl_init": OnFunctionExecutedCurl, + "curl_setopt": OnFunctionExecutedCurl, + + "exec": OnFunctionExecutedShell, + "shell_exec": OnFunctionExecutedShell, + "system": OnFunctionExecutedShell, + "passthru": OnFunctionExecutedShell, + "popen": OnFunctionExecutedShell, + "proc_open": OnFunctionExecutedShell, +} + +func OnFunctionExecuted(data map[string]interface{}) string { + functionName := MustGetFromMap[string](data, "function_name") + parameters := MustGetFromMap[map[string]interface{}](data, "parameters") + + CheckIfKeyExists(functionExecutedHandlers, functionName) + + return functionExecutedHandlers[functionName](parameters) +} diff --git a/src/lib/handle_shell_execution.go b/src/lib/handle_shell_execution.go new file mode 100644 index 00000000..f9afdc35 --- /dev/null +++ b/src/lib/handle_shell_execution.go @@ -0,0 +1,15 @@ +package main + +import "fmt" + +var shellCommands = map[string]bool{} + +func OnFunctionExecutedShell(parameters map[string]interface{}) string { + cmd := GetFromMap[string](parameters, "cmd") + if cmd == nil { + return "{}" + } + shellCommands[*cmd] = false + fmt.Println("[AIKIDO-GO] Got shell command:", *cmd) + return "{}" +} diff --git a/src/lib/handle_urls.go b/src/lib/handle_urls.go new file mode 100644 index 00000000..cca2d29b --- /dev/null +++ b/src/lib/handle_urls.go @@ -0,0 +1,16 @@ +package main + +import "fmt" + +var outgoingHostnames = map[string]bool{} + +func OnFunctionExecutedCurl(parameters map[string]interface{}) string { + url := GetFromMap[string](parameters, "url") + if url == nil { + return "{}" + } + domain := GetDomain(*url) + outgoingHostnames[domain] = false + fmt.Println("[AIKIDO-GO] Got domain:", domain) + return "{}" +} diff --git a/src/lib/main.go b/src/lib/main.go new file mode 100644 index 00000000..79341b27 --- /dev/null +++ b/src/lib/main.go @@ -0,0 +1,40 @@ +package main + +import "C" +import ( + "encoding/json" + "fmt" +) + +type eventFunctionExecutedFn func(map[string]interface{}) string + +var eventHandlers = map[string]eventFunctionExecutedFn{ + "function_executed": OnFunctionExecuted, +} + +//export OnEvent +func OnEvent(eventJson string) (outputJson string) { + defer func() { + if r := recover(); r != nil { + fmt.Println("[AIKIDO-GO] Recovered from panic:", r) + outputJson = "{}" + } + }() + + fmt.Println("[AIKIDO-GO] OnEvent:", eventJson) + + var event map[string]interface{} + err := json.Unmarshal([]byte(eventJson), &event) + if err != nil { + panic(fmt.Sprintf("Error parsing JSON: %s", err)) + } + + eventName := MustGetFromMap[string](event, "event") + data := MustGetFromMap[map[string]interface{}](event, "data") + + CheckIfKeyExists(eventHandlers, eventName) + + return eventHandlers[eventName](data) +} + +func main() {} diff --git a/src/lib/utils.go b/src/lib/utils.go new file mode 100644 index 00000000..6b53ba03 --- /dev/null +++ b/src/lib/utils.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "net/url" +) + +func CheckIfKeyExists[K comparable, V any](m map[K]V, key K) { + if _, exists := m[key]; !exists { + panic(fmt.Sprintf("Key %s does not exist in map!", key)) + } +} + +func GetFromMap[T any](m map[string]interface{}, key string) *T { + value, ok := m[key] + if !ok { + return nil + } + result, ok := value.(T) + if !ok { + return nil + } + return &result +} + +func MustGetFromMap[T any](m map[string]interface{}, key string) T { + value := GetFromMap[T](m, key) + if value == nil { + panic(fmt.Sprintf("Error parsing JSON: key %s does not exist or it has an incorrect type", key)) + } + return *value +} + +func GetDomain(rawurl string) string { + parsedURL, err := url.Parse(rawurl) + if err != nil { + return "" + } + return parsedURL.Hostname() +} diff --git a/tests/test_shell_execution.php b/tests/test_shell_execution.php new file mode 100644 index 00000000..f188ce71 --- /dev/null +++ b/tests/test_shell_execution.php @@ -0,0 +1,55 @@ + array("pipe", "r"), // stdin is a pipe that the child will read from + 1 => array("pipe", "w"), // stdout is a pipe that the child will write to + 2 => array("pipe", "w") // stderr is a pipe that the child will write to +); + +$process = proc_open('echo "Hello from proc_open!"', $descriptorspec, $pipes); + +if (is_resource($process)) { + while ($s = fgets($pipes[1])) { + echo $s; + } + fclose($pipes[1]); + proc_close($process); +} +echo "\n"; +?>