2015/1/16

about sql injection


實例講解 SQL 注入攻擊

 http://blog.jobbole.com/83092/


一位客戶讓我們針對只有他們企業員工和顧客能使用的企業內網進行滲透測試。這是安全評估的一個部分,所以儘管我們之前沒有使用過SQL注入來滲透網絡,但對其概念也相當熟悉了。最後我們在這項任務中大獲成功,現在來回顧一下這個過程的每一步,將它記錄為一個案例。
「SQL注入」是一種利用未過濾/未審核用戶輸入的攻擊方法(「緩存溢出」和這個不同),意思就是讓應用運行本不應該運行的SQL代碼。如果應用毫無防備地創建了SQL字符串並且運行了它們,就會造成一些出人意料的結果。
我們記錄下了在多次錯誤的轉折後經歷的曲折過程,而一個更有經驗的人會有這不同的 — 甚至更好的 — 方法。但事實上我們成功以後才明白,我們並沒有完全被誤導。
其他的SQL文章包含了更多的細節,但是這篇文章不僅展示了漏洞利用的過程,還講述了發現漏洞的原理。

目標內網

展現在我們眼前的是一個完整定製網站,我們之前沒見過這個網站,也無權查看它的源代碼:這是一次「黑盒」攻擊。『刺探』結果顯示這台服務器運行在微 軟的IIS6上,並且是ASP.NET架構。這就暗示我們數據庫是微軟的SQL server:我們相信我們的技巧可以應用在任何web應用上,無論它使用的是哪種SQL 服務器。
登陸頁有傳統的用戶-密碼表單,但多了一個 「把我的密碼郵給我」的鏈接;後來,這個地方被證實是整個系統陷落的關鍵。
當鍵入郵件地址時,系統假定郵件存在,就會在用戶數據庫裡查詢郵件地址,然後郵寄一些內容給這個地址。但我的郵件地址無法找到,所以它什麼也不會發給我。
對於任何SQL化的表單而言,第一步測試,是輸入一個帶有單引號的數據:目的是看看他們是否對構造SQL的字符串進行了過濾。當把單引號作為郵件地址提交以後,我們得到了500錯誤(服務器錯誤),這意味著「有害」輸入實際上是被直接用於SQL語句了。就是這了!
我猜測SQL代碼可能是這樣:
1
2
3
SELECT fieldlist
  FROM table
 WHERE field = '$EMAIL';
$EMAIL 是用戶從表單提交的地址,並且這段查詢在字符串末端$EMAIL上提供了引號。我們不知道字段或表的確切名字,但是我們瞭解他們的本質,這有助於我們做正確的猜測。
當我們鍵入steve@unixwiz.net『 -注意這個末端的引號 – 下面是這個SQL字段的構成:
1
2
3
SELECT fieldlist
  FROM table
 WHERE field = 'steve@unixwiz.net'';
當這段SQL開始執行,SQL解析器就會發現多餘的引號然後中斷執行,並給出語法錯誤的提示。這個錯誤如何清楚的表述給用戶,基於應用內部的錯誤恢 復規程,但一般來說都不會提示「郵件地址不存在」。這個錯誤響應成了死亡之門,它告訴別人用戶輸入沒有被正確的處理,這就為應用破解留下了可乘之機。
這個數據呈現在WHERE的從句中,讓我們以符合SQL規範的方式改變輸入試試,看看會發生什麼。鍵入anything' OR 『x'=『x, 結果如下:
1
2
3
SELECT fieldlist
  FROM table
 WHERE field = 'anything' OR 'x'='x';
因為應用不會思考輸入 – 僅僅構造字符串 - 我們使用單引號把WHERE從句的單一組成變成了雙組成,』x'=『x從句是恆成立的,無論第一個從句是什麼。(有一種更好的方式來確保「始終為真」,我們隨後會接觸到)。

但與每次只返回單一數據的「真實」查詢不同,上面這個構造必須返回這個成員數據庫的所有數據。要想知道在這種情況下應用會做什麼,唯一的方法就是嘗試,嘗試,再嘗試。我們得到了這個:

你的登錄信息已經被郵寄到了 random.person@example.com.

我們猜測這個地址是查詢到的第一條記錄。這個傢伙真的會在這個郵箱裡收到他忘記的密碼,想必他會很吃驚也會引起他的警覺。
我們現在知道可以根據自己的需要來篡改查詢語句了,儘管對於那些看不到的部分還不夠瞭解,但是我們注意到了在多次嘗試後得到了三條不同的響應:
  • 「你的登錄信息已經被郵寄到了郵箱」
  • 「我們不能識別你的郵件地址」
  • 服務器錯誤
前兩個響應是有效的SQL,最後一個響應是無效的SQL:當猜測查詢語句結構的時候,這種區別非常有用。

模式字段映射

第一步是猜測字段名:我們合理的推測了查詢包含「email address」和「password」,可能也會有「US Mail address」或者「userid」或「phone number」這樣的字段。我們特別想執行 SHOW TABLE語句, 但我們並不知道表名,現在沒有比較明顯的辦法可以拿到表名。
我們進行了下一步。在每次測試中,我們會用我們已知的部分加上一些特殊的構造語句。我們已經知道這個SQL的執行結果是email地址的比對,因此我們來猜測email的字段名:
1
2
3
SELECT fieldlist
  FROM table
 WHERE field = 'x' AND email IS NULL; --';
目的是假定的查詢語句的字段名(email),來試試SQL是不是有效。我不關心匹配的郵件地址是啥(我們用了個偽名』x'),——』這個符號表示SQL註釋的起始。對於去除應用末尾提供的引號,這是一個很有效的方式,我們不用在乎我們屏蔽掉的是啥。
如果我們得到了服務器錯誤,意味著SQL有不恰當的地方,並且語法錯誤會被拋出:更有可能是字段名有錯。如果我們得到了任何有效的響應,我們就可以 猜測這個字段名是正確的。這就是我們得到「email unknown」或「password was sent」響應的過程。
我們也可以用AND連接詞代替OR:這是有意義的。在SQL的模式映射階段,我們不需要為猜一個特定的郵件地址而煩惱,我們也不想應用隨機的氾濫的 給用戶發「這是你的密碼」的郵件 - 這不太好,有可能引起懷疑。而使用AND連接郵件地址,就會變的無效,我們就可以確保查詢語句總是返回0行,永遠不會生成密碼提醒郵件。
提交上面的片段的確給了我們「郵件地址未知」的響應,現在我們知道郵件地址的確是存儲在email字段名裡。如果沒有生效,我們可以嘗試email_address或mail這樣的字段名。這個過程需要相當多的猜測。
接下來,我們猜測其他顯而易見的名字:password,user ID, name等等。每次只猜一個字段,只要響應不是「server failure」,那就意味著我們猜對了。
1
2
3
SELECT fieldlist
  FROM table
 WHERE email = 'x' AND userid IS NULL; --';
在這個過程中,我們找到了幾個正確的字段名:
  • email
  • passwd
  • login_id
  • full_name
無疑還有更多(有一個線索是表單中的字段名),一陣挖掘後沒有發現更多了。但是我們依然不知道這些字段名的表名,它們在哪找到的?
尋找數據庫表名
應用的內建查詢指令已經建立了表名,但是我們不知道是啥:有幾個方法可以找到表名。其中一個是依靠subselect(字查詢)。
一個獨立的查詢
1
SELECT COUNT(*) FROM tabname
返回表裡記錄的數量,如果表名無效,查詢就會失敗。我們可以建立自己的字符串來探測表名:
1
2
3
SELECT email, passwd, login_id, full_name
  FROM table
 WHERE email = 'x' AND 1=(SELECT COUNT(*) FROM tabname); --';
我們不關心到底有多少條記錄,只關心表名是不是正確。重複多次猜測以後,我們終於發現members是這個數據庫裡的有效表名。但它是用在這個查詢裡的麼?所以我們需要另一個測試,使用table.field:實際查詢的部分只工作在這個表中,而不是只要表存在就執行。
1
2
3
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = 'x' AND members.email IS NULL; --';
當返回「Email unknown」時,就意味著我們的SQL注入成功了,並且我們正確的猜測出了表名。這對後面的工作很重要,但是我們暫時先試試其他的方法。
找用戶賬號
我們對members表的結構有了一個局部的概念,但是我們僅知道一個用戶名:任意用戶都可能得到「Here is your password」的郵件。回想起來,我們從未得到過信息本身,只有它發送的地址。我們得再弄幾個用戶名,這樣就能得到更多的數據。
首先,我們從公司網站開始找幾個人:「About us」或者「Contact」頁通常提供了公司成員列表。通常都包含郵件地址,即使它們沒有提供這個列表也沒關係,我們可以根據某些線索用我們的工具找到它們。
LIKE從句可以進行用戶查詢,允許我們在數據庫裡局部匹配用戶名或郵件地址,每次提交如果顯示「We sent your password」的信息並且郵件也真發了,就證明生效了。
警告:這麼做拿到了郵件地址,但也真的發了郵件給對方,這有可能引起懷疑,小心使用。
我們可以查詢email name或者full name(或者推測出來的其他信息),每次放入%通配符進行如下查詢:
1
2
3
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = 'x' OR full_name LIKE '%Bob%';
記住儘管可能不只有一個「Bob」,但我們只能看到一條信息:建議精煉LIKE從句。
密碼暴力破解
可以肯定的是,我們能在登陸頁進行密碼的暴力破解,但是許多系統都針對此做了監測甚至防禦。可能有的手段有操作日誌,帳號鎖定,或者其他能阻礙我們行動的方式,但是因為存在未過濾的輸入,我們就能繞過更多的保護措施。
我們在構造的字符串裡包含進郵箱名和密碼來進行密碼測試。在我們的例子中,我們用了受害者bob@example.com 並嘗試了多組密碼。
1
2
3
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = '<a href="mailto:bob@example.com">bob@example.com</a>' AND passwd = 'hello123';
這是一條很好使的SQL語句,我們不會得到服務器錯誤的提示,只要我們得到「your password has been mailed to you」的提示信息,就證明我們已經得到密碼了。這時候受害人可能會警覺起來,但誰關心他呢,我們已經得到密碼了。
這個過程可以使用perl腳本自動完成,然而,我們在寫腳本的過程中,發現了另一種方法來破解系統。

數據庫不是只讀的

迄今為止,我們沒做查詢數據庫之外的事,儘管SELECT是只讀的,但不代表SQL只能這樣。SQL使用分號表示結束,如果輸入沒有正確過濾,就沒有什麼能阻止我們在字符串後構造與查詢無關的指令。
The most drastic example is:
這劑猛藥是這樣的:
1
2
3
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = 'x'; DROP TABLE members; --';  -- Boom!
第一部分我們準備了一個偽造的email地址——『X'——我們不關心查詢結果返回什麼:我們想要得到的只是我們自己構造的SQL指令。這次攻擊刪除了整個members表,這就不太好玩了。
這表明我們不僅僅可以切分SQL指令,而且也可以修改數據庫。這是被允許的。

添加新用戶

我們已經瞭解了members表的局部結構,添加一條新紀錄到表裡視乎是一個可行的方法:如果這成功了,我們就能簡單的用我們新插入的身份登陸到系統了。
不要太驚訝,這條SQL有點長,我們把它分行顯示以便於理解,但它依然是一條語句:
1
2
3
4
5
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = 'x';
        INSERT INTO members ('email','passwd','login_id','full_name')
        VALUES ('steve@unixwiz.net','hello','steve','Steve Friedl');--';
即使我們得到了正確的字段名和表名,但在成功攻擊之前我們還有幾件事需要瞭解:
  1. 在web表單裡,我們可能沒有足夠的空間鍵入這麼多文本(儘管可以用腳本解決,但並不容易)。
  2. web應用可能沒有members表的INSERT權限。
  3. 母庸置疑,members表裡肯定還有其他字段,有一些可能需要初始值,否則會引起INSERT失敗。
  4. 即使我們插入了一條新紀錄,應用也可能不正常運行,因為我們無法提供值的字段名會自動插入NULL。
  5. 一個正確的「member」可能額不僅僅只需要members表裡的一條紀錄,而還要結合其他表的信息(如,訪問權限),因此只添加一個表可能不夠。
在這個案例裡,我們遇到了問題#4或#5,我們無法確定到底是哪個—— 因為用構造好的用戶名登陸進去的時候,返回了服務器錯誤的提示。儘管這就暗示了我們那些沒有構造的字段是必須的,但我們沒有辦法正確處理。
一個可行的辦法是猜測其他字段,但這是一個勞力費神的過程:儘管我們可以猜測其他「顯而易見」的字段,但要想得到整個應用的組織結構圖太難了。
我們最後嘗試了其他方式。
把密碼郵給我
我們意識到雖然我們無法添加新紀錄到members數據庫裡,但我們可以修改已經存在的,這被證明是可行的。
從上一步得知 bob@example.com 賬戶在這個系統裡,我們用SQL注入把數據庫中的這條記錄改成我們自己的email地址:
1
2
3
4
5
6
SELECT email, passwd, login_id, full_name
  FROM members
 WHERE email = 'x';
      UPDATE members
      SET email = <a href="mailto:'steve@unixwiz.net">'steve@unixwiz.net</a>'
      WHERE email = <a href="mailto:'bob@example.com">'bob@example.com</a>';
運行之後,我們自然得到了「we didn't know your email address」的提示,但這在預料之中,畢竟我們用了假的email地址。UPDATE操作不會通知應用,因此它悄然執行了。
之後,我們使用了「I lost my password」的功能,用我們剛剛更新的email地址,一分鐘後,我們收到了這封郵件:
1
2
3
4
5
6
7
From: <a href="mailto:system@example.com">system@example.com</a>
To: <a href="mailto:steve@unixwiz.net">steve@unixwiz.net</a>
Subject: Intranet login
 
This email is in response to your request for your Intranet log in information.
Your User ID is: bob
Your password is: hello
現在,我們要做的就是跟隨標準的登錄流程進入系統,這是一個高等級職員,有高級權限,比我們INSERT的用戶要好。
我們發現這個企業內部站點內容特別多,甚至包含了一個全用戶列表,我們可以合理的推出許多內網都有同樣的企業Windows網絡帳號,它們可能在所 有地方都使用同樣的密碼。我們很容易就能得到任意的內網密碼,並且我們找到了企業防火牆上的一個開放的PPTP協議的VPN端口,這讓登錄測試變得更簡 單。
我們又挑了幾個帳號測試都沒有成功,我們無法知道是否是「密碼錯誤」或者「企業內部帳號是否與Windows帳號名不同」。但是我們覺得自動化工具會讓這項工作更容易。

其他方法

在這次特定的滲透中,我們得到了足夠的權限,我們不需要更多了,但是還有其他方法。我們來試試我們現在想到的但不夠普遍的方法。
我們意識到不是所有的方法都與數據庫無關,我們可以來試試。
調用xp_cmdshell
微軟的SQLServer支持存儲過程xp_cmdshell有權限執行任意操作系統指令。如果這項功能允許web用戶使用,那webserver被滲透是無法避免的。
迄今為止,我們做的都被限制在了web應用和數據庫這個環境下,但是如果我們能執行任何操作系統指令,再厲害的服務器也禁不住滲透。xp_cmdshell通常只有極少數的管理員賬戶才能使用,但它也可能授權給了更低級的用戶。
繪製數據庫結構
在這個登錄後提供了豐富功能應用上,已經沒必要做更深的挖掘了,但在其他限制更多的環境下可能還不夠。
能夠系統的繪製出數據庫可見結構,包含表和它們的字段結構,可能沒有直接幫助。但是這為網站滲透提供了一條林萌大道。
從網站的其他方面收集更多有關數據結構的信息(例如,「留言板」頁?「幫助論壇」等?)。不過這對應用環境依賴強,而且還得靠你準確的猜測。
減輕危害
我們認為web應用開發者通常沒考慮到「有害輸入」,但安全人員應該考慮到(包括壞傢伙),因此這有3條方法可以使用。
輸入過濾
過濾輸入是非常重要的事,以確保輸入不包含危險代碼,無論是SQL服務器或HTM本身。首先想到的是剝掉「惡意字符」,像引號、分號或轉義符號,但這是一種不太好的方式。儘管找到一些危險字符很容易,但要把他們全找出來就難了。
web語言本身就充滿了特殊字符和奇怪的標記(包括那些表達同樣字符的替代字符),所以想要努力識別出所有的「惡意字符」不太可能成功。
換言之,與其「移除已知的惡意數據」,不如移除「良好數據之外的所有數據」:這種區別是很重要的。在我們的例子中,郵件地址僅能包含如下字符:
1
2
3
4
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
@.-_+
允許不正確的字符輸入是沒有任何好處的,應該早點拒絕它們 - 可能會有一些錯誤信息 – 不僅可以阻止SQL注入,也可以捕獲一些輸入錯誤而不是把它們存入數據庫。
某個特殊的email地址會讓驗證程序陷入麻煩,因為每個人對於「有效」的定義不同。由於email地址中出現了一個你沒有考慮到的字符而被拒絕,那真是糗大了。
真正的權威是RFC 2822(比RFC822內容還多),它對於」允許使用的內容「做了一個規範的定義。這種更學術的規範希望可以接受&和*(還有更多)作為有效的email地址,但其它人 - 包括作者 – 都樂於用一個合理的子集來包含更多的email地址。
那些採用更限制方法的人應當充分意識到沒有包含這些地址會帶來的後果,特別是限制有了更好的技術(預編譯/執行,存儲過程)來避免這些「奇怪」的字符帶來的安全問題。
意識到「過濾輸入」並不意味著僅僅是「移除引號」,因為即使一個「正規」的字符也會帶來麻煩。在下面這個例子中,一個整型ID值被拿來和用戶的輸入作比較(數字型PIN):
1
2
3
SELECT fieldlist
  FROM table
 WHERE id = 23 OR 1=1;  -- Boom! Always matches!
在實踐中,無論如何這個方法都有諸多限制,因為能夠徹底排除所有危險字符的字段實在太少了。對於「日期」或者「email地址」或者「整型」,上面的辦法是有價值的,但對於真實的環境,我們不可避免地要使用其他方式來減輕危害。
輸入項編碼/轉義
現在可以過濾電話號碼和郵件地址了,但你不能通過同樣的方法處理「name」字段,要不然可能會排除掉Bill O'Reilly這樣的名字:對於這個字段,這裡的引號是合法的輸入。
有人就想到過濾到單引號的時候,再加上一個引號,這樣就沒問題了 – 但是這麼幹要出事啊!
預處理每個字符串來替換單引號:
1
2
3
SELECT fieldlist
  FROM customers
 WHERE name = 'Bill O''Reilly'-- works OK
這個方法很容易出問題,因為大部分數據庫都支持轉碼機制。像MySQL,允許輸入』來替代單引號,因此如果輸入 『; DROP TABLE users;  時,通過兩次引號來「保護」數據庫,那我們將得到:
1
2
3
SELECT fieldlist
  FROM customers
 WHERE name = '\''; DROP TABLE users; --';  -- Boom!
『』』 是 一個完整的SQL語句(只包含一個引號),通常,惡意SQL代碼就會緊跟其後。不光是反斜線符號的情況:像Unicode編碼,其他的編碼或者解析規則都 會無意中給程序員挖坑。完美的過濾的很困難的,這就是為什麼許多的數據庫藉口語言都提供函數給你使用。當同樣的內容給「string quoting」和「string parsing」處理過後,會好一些,也更安全一些。
比如MySQL的函數mysql_real_escape_string()和perl DBD 的 $dbh->quote($value)方法,這些方法都是必用的。
參數綁定 (預編譯語句)
儘管轉義是一個有用的機制,但我們任然處於「用戶輸入被當做SQL語句」這麼一個循環裡。更好的方法是:預編譯,本質上所有的數據庫編程接口都支持預編譯。技術上來說,SQL聲明語句是用問號給每個參數佔位創建的 – 然後在內部表中進行編譯。
預編譯查詢執行時是按照參數列表來的:
Perl中的例子
1
2
3
$sth = $dbh->prepare("SELECT email, userid FROM members WHERE email = ?;");
 
$sth->execute($email);
感謝Stefan Wagner幫我寫了個java實現:
不安全版
1
2
3
Statement s = connection.createStatement();
ResultSet rs = s.executeQuery("SELECT email FROM member WHERE name = "
                             + formField); // *boom*
安全版
1
2
3
4
PreparedStatement ps = connection.prepareStatement(
    "SELECT email FROM member WHERE name = ?");
ps.setString(1, formField);
ResultSet rs = ps.executeQuery();
$email 是從用戶表單獲得的,它作為#1(第一個問號標記的地方)位置的參數傳遞過來,在任何情況下這條SQL聲明都可以解 析。引號,分號,反斜槓,SQL指令記號 – 任何字符都不會產生特殊效果,因為它們「只是數據」而已。這不會對其他東西造成破壞,因此這個應用很大程度上防範了SQL注入攻擊。
如果預編譯查詢語句多次(只編譯一次)執行,也會帶來性能上的提升,但是與大量安全方面的巨大提升相比,這顯得微不足道。這可能是我們保證web應用安全最重要的一步。
限制數據庫權限和隔離用戶
在這個案例中,我們觀察到只有兩個交互動作不在登錄用戶的上下文環境中:「登錄」和「發密碼給我」。web應用應該對數據庫連接做權限的限制:對於members表只能讀,並且無法操作其他表。
作用是即使一次「成功的」SQL注入攻擊也只能得到非常有限的成功。噢,我們將不能做有授權的UPDATE請求,我們要求助於其他方法。
一旦web應用確定登錄表單傳遞來的認證是有效的,它就會切換會話到一個有更多權限的用戶上。
對任何web應用而言,不使用sa權限幾乎是根本不用說的事。
對數據庫的訪問採用存儲過程
如果數據庫支持存儲過程,請使用存儲過程來執行數據庫的訪問行為,這樣就不需要SQL了(假設存儲過程編程正確)。
把查詢,更新,刪除等動作規則封裝成一個單獨的過程,就可以針對基礎規則和所執行的商業規則來完成測試和歸檔(例如,如果客戶超過了信用卡限額,「添加新記錄」過程可能拒絕訂單)。
對於簡單的查詢這樣做可能僅僅能獲得很少的好處,不過一旦操作變複雜(或者被用在更多地方),給操作一個單獨的定義,功能將會變得更穩健也更容易維護。
注意:動態構建一個查詢的存儲過程是可以做到的:這麼做無法防止SQL注入 – 它只不過把預編譯/執行綁定到了一起,或者是把SQL語句和提供保護的變量綁定到了一起。
隔離web服務器
實施了以上所有的防禦措施,仍然可能有某些地方有遺漏,導致了服務器被滲透。設計者應該在假定壞蛋已經獲得了系統最高權限下來設計網絡設施,然後把它的攻擊對其他事情產生的影響限制在最小。
例如,把這台機器放置在極度限制出入的DMZ網絡「內部」,這麼做意味著即便取得了web服務器的完全控制也不能自動的獲得對其他一切的完全訪問權限。當然,這麼做不能阻止所有的入侵,不過它可以使入侵變的非常困難。
配置錯誤報告
一些框架的錯誤報告包含了開發的bug信息,這不應該公開給用戶。想像一下:如果完整的查詢被現實出來了,並且指出了語法錯誤點,那要攻擊該有多容易。
對於開發者來說這些信息是有用的,但是它應該禁止公開 – 如果可能 - 應該限制在內部用戶訪問。
注意:不是所有的數據庫都採用同樣的方式配置,並且不是所有的數據庫都支持同樣的SQL語法(「S」代表「結構化」,不是「標準的」)。例如,大多 數版本的MySQL都不支持子查詢,而且通常也不允許單行多條語句(multiple statements):當你滲透網絡時,實際上這些就是使問題複雜化的因素。

再強調一下,儘管我們選擇了「忘記密碼」鏈接來試試攻擊,但不是因為這個功能不安全。而是幾個易攻擊的點之一,不要把焦點聚集在「忘記密碼」上。
這個教學示例不準備全面覆蓋SQL注入的內容,甚至都不是一個教程:它僅僅是一篇我們花了幾小時做的滲透測試的記錄。我們看了其他的關於SQL注入文章的討論,但它們只給出了結果而沒有給出過程。
但是那些結果報告需要技術背景才能看懂,並且滲透細節也是有價值的。在沒有源代碼的情況下,滲透人員的黑盒測試能力也是有價值的。
感謝 David Litchfield 和 Randal Schwartz對本文的貢獻,還有Chris Mospaw的排版(© 2005 by Chris Mospaw, used with permission).

其他資源

關於作者: zer0Black@lxtalx

關注信息安全,網絡安全,目前為移動開發工程師,android和IOS兼有涉獵。半路出道,基礎薄弱,正努力補習計算機基礎中。近日習得「遍歷」學習法,正欲嘗試之。(新浪微博:@Zer0Black
查看zer0Black的更多文章 >>

2015/1/9

同時開啟兩個Excel檔案於多螢幕並列顯示

來源自  黑暗執行緒:

同時開啟兩個Excel檔案於多螢幕並列顯示

Excel有個討厭特性,開啟多份Excel檔案時不像Word每個文 件一個視窗,而是預設開在同一視窗,每次只能顯示其中一個檔案,如果要左右並陳對照,可以取消個檔案的最大化,在同一個視窗中並列,只是這麼做不能善用多 螢幕的優勢,雖然可以將視窗拉大橫跨兩個螢幕,但有螢幕解析不同,以及無法使用快捷鍵調整視窗尺寸位置的問題,用起來不甚順手,還是覺得像Word一樣各 自成為獨立視窗操作比較方便。
平日上班使用Excel 2010,有個解決方案不要直接點選Excel澢,而由程式集的Exce捷徑l開啟新l視窗,點選兩次就會產生兩個獨立的Excel視窗,再透過各自的 【檔案/開啟舊檔】選取不同Excel檔開啟,就能實現同時開啟兩個Excel檔並列顯示。(但會有些限制,例如: 工作表不能在兩個Excel檔間複製或搬移)
先啟動Excel再開啟Excel檔有點繁瑣,網路上有人找到另一種解法: 修改或增加Excel檔Shell指令,允許Excel檔在新的Excel視窗開啟,操作起來如下圖:
要增加Shell指令,需修改Registry。將以下的內容存成.reg,再點選執行即可為Excel檔新增"在新視窗開啟"選項。注意: 實際EXCEL.EXE安裝路徑可能有所差異,請自行依實際位置修改調整。
排版顯示純文字複製文字
以上範例中,Excel.Sheet.8為xls檔、Excel.Sheet.12為xlsx檔,如要為xlsm等格式也加上指令,可自行比照設定,但實務上設定xls及xlsx應該就夠用。
PS: 終於,Excel 2013起改為一個檔案一個視窗(SDI模式),要在多螢幕下兩個檔案並列對照方便多了。

JPA+complex key+custom Query

  來源: https://www.cnblogs.com/520playboy/p/6512592.html   整個來說,就是有複合主鍵 然後要使用  public interface XxXXxx DAO extends CrudRepository<Tc...