Java程式碼審計匯總系列(一)——SQL注入
- 2019 年 11 月 7 日
- 筆記

一、程式碼審計
相比黑盒滲透的漏洞挖掘方式,程式碼審計具有更高的可靠性和針對性,更多的是依靠對程式碼、架構的理解;使用的審計工具一般選擇Eclipse或IDEA;審計工作過程主要有三步:風險點發現——>風險定位追蹤——>漏洞利用,所以審計不出漏洞無非就是find:「找不到該看哪些程式碼」和judge:「定位到程式碼但判斷不出有沒有問題」。而風險點發現的重點則在於三個地方:用戶輸入(入參)處+檢測繞過處+漏洞觸發處,一般審計程式碼都是藉助程式碼掃描工具(Fortify/Checkmarx)或從這三點著手。
本系列選取WebGoat作為案例,講解漏洞的特徵發現、定位技巧、調試及觸發利用的具體過程,盡量涵蓋所有的挖掘場景,最後補充實戰挖掘案例。
二、SQLi漏洞挖掘
1、介紹
SQLi是最著名也是影響最廣的漏洞之一,注入漏洞都是程式把用戶輸入的數據當做程式碼執行,發現的關鍵有兩個,第一是用戶能夠控制輸入;第二是用戶輸入的數據被拼接到要執行的程式碼中從而被執行。
2、挖掘過程
這裡以webgoat的數字型注入講解SQLi漏洞的挖掘過程:

1) 定位特定功能模組的程式碼
了解不同框架特性,本系統的Springboot註解:
@RequestMapping(path= PATH)
@GetMapping(path= PATH)
@PostMapping(path= PATH)
通過抓取請求數據包獲取path特徵SqlInjection/assignment5b:

使用IDEA的全局搜索功能(SHIFT+CTRL+F)定位到程式碼:

2) 程式碼分析
SqlInjectionLesson5b.java類程式碼如下:
@PostMapping("/SqlInjection/assignment5b") @ResponseBody public AttackResult completed(@RequestParam String userid, @RequestParam String login_count, HttpServletRequest request) throws IOException { return injectableQuery(login_count, userid); } protected AttackResult injectableQuery(String login_count, String accountName) { String queryString = "SELECT * From user_data WHERE Login_Count = ? and userid= " + accountName; try { Connection connection = DatabaseUtilities.getConnection(getWebSession()); PreparedStatement query = connection.prepareStatement(queryString, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
這是一個典型的動態拼接用戶輸入和防範案例,系統接收兩個參數login_count和userid,其中login_count通過「+」直接拼接,而userid首先通過類型轉換為Integer賦值給count,並經過了預編譯(參數化請求)處理,不存在SQLi漏洞。
3)漏洞驗證
最後構造路徑及參數POC驗證漏洞存在:

3、漏洞分類挖掘技巧
根據挖掘經驗,白盒挖掘層面大致可以將SQLi的類型分為六類:
1、入參直接動態拼接;
2、預編譯有誤;
3、框架注入(Mybatis+Hibernate);
4、order by 繞過預編譯;
5、%和_繞過預編譯;
6、SQLi檢測繞過
1) 參數直接拼接
最明顯的「+」拼接,思路一般有二:通過關鍵字定位到SQL語句,回溯參數是否是用戶可控;或通過跟蹤用戶輸入,是否執行SQL操作,搜索的關鍵詞有:
Select|insert|update|delete|java.sql.Connection|Statement|.execute|.executeQuery|jdbcTemplate|queryForInt|queryForObject|queryForMap|getConnection|PreparedStatement|Statement|execute|jdbcTemplate|queryForInt|queryForObject|queryForMap|executeQuery|getConnection
2) 預編譯有誤
並不是使用了預編譯PreparedStatement一定就可以防止SQL注入,動態拼接SQL同樣存在SQLi注入,這也是實際審計中高發的問題,下面程式碼就是典型的預編譯有誤:
String query = "SELECT * FROM usersWHERE userid ='"+ userid + "'" + " AND password='" +password + "'"; PreparedStatement stmt =connection.prepareStatement(query); ResultSet rs = stmt.executeQuery();
定位預編譯可以通過搜索關鍵函數:
setObject()、setInt()、setString()、setSQLXML()
3) 框架注入
Hibernate典型的注入程式碼為:
session.createQuery("from Book wheretitle like '%" + userInput + "%' and published = true")
或形如:
{ StringBuffer queryString = newStringBuffer(); queryString.append(「from Test where id=』」); queryString.append(id); queryString.append(『』』); }
定位此框架的SQL注入首先需要在xml配置文件或import包里確認是否使用此框架,然後使用關鍵字createQuery,session.save(,session.update(,session.delete進行定位。
Mybatis有兩種變數方法,不安全的寫法為:
select * from books where id= ${id}
安全的寫法為JDBC預編譯:
select * from books where id= #{id}
此外like、in和order by語句也需要使用#,挖掘技巧則是在註解中或者Mybatis相關的配置文件中搜索 $。
4) order by 繞過預編譯
類似下面sql語句 order by 後面是不能用預編譯處理的只能通過拼接處理,只能手動進行過濾,詳見案例。
String sql = 「Select * from news where title =?」+ 「order by『」 + time + 「』asc」
5) %和_繞過預編譯
預編譯是不能處理%,需要手動過濾,否則會造成慢查詢和DOS。
6) SQLi檢測繞過
若SQL在處理過程中經過黑/白名單(正則)或Filter檢測,通常檢測程式碼存在缺陷則可進行檢測繞過。
4、漏洞防禦
OWASP官方推薦的SQLi防禦方案有四種:
1)預編譯(參數化查詢)
PreparedStatement stmt =connection.prepareStatement("SELECT * FROM users WHERE userid=? ANDpassword=?"); stmt.setString(1, userid); stmt.setString(2, password); ResultSet rs = stmt.executeQuery();
2)存儲過程
使用CallableStatement對存儲過程介面的實現來執行資料庫查詢,SQL程式碼定義並存儲在資料庫本身中,然後從應用程式中調用,使用存儲過程和預編譯在防SQLi方面的效果是相同的。
String custname =request.getParameter("customerName"); try { CallableStatement cs = connection.prepareCall("{callsp_getAccountBalance(?)}"); cs.setString(1, custname); ResultSet results = cs.executeQuery(); } catch (SQLException se) { }
3)黑/白名單驗證
屬於輸入驗證的範疇,大多使用正則表達式限制,或對於諸如排序順序之類的簡單操作,最好將用戶提供的輸入轉換為布爾值,然後將該布爾值用於選擇要附加到查詢的安全值。
public String someMethod(boolean sortOrder) { String SQLquery = "someSQL ... order by Salary " + (sortOrder ? "ASC" :"DESC");`
4) 輸出轉義
將用戶輸入放入查詢之前對其進行轉義,OWASP企業安全性API(ESAPI)是一個免費的開源Web應用程式安全控制庫。
CodecORACLE_CODEC = new OracleCodec(); Stringquery = "SELECT user_id FROM user_data WHERE user_name = '" +ESAPI.encoder().encodeForSQL( ORACLE_CODEC,req.getParameter("userID")) + "'and user_password = '" + ESAPI.encoder().encodeForSQL( ORACLE_CODEC,req.getParameter("pwd")) +"'";
5)框架修復:
對於Mybatis框架:
select * from news where tile like concat(『%』,#{title}, 『%』),
select * from news where id in <foreach collection="ids"item="item"open="("separator=","close=")">#{item}</foreach>
Mybatis的order by語句可以選擇在java層做映射或過濾用戶輸入進行防禦。
對於Hibernate(HQL)框架預編譯:
方法一:
Query query=session.createQuery(「from Useruser where user.name=:customername and user:customerage=:age 」); query.setString(「customername」,name); query.setInteger(「customerage」,age);
方法二:
String hql ="FROM User user where user.name=? and user.age=?"; Query q =session.createQuery(hql); q.setString(0, name); q.setInteger(1,age);
5、實戰案例
1) Mybatis框架
對文章刪除功能進行審計,articelId參數前端可控:
@RequestMapping("/delete") public ModelAndView delete(HttpServletRequestrequest) { ModelAndView model = newModelAndView(); try { model.setViewName(this.getRequestUri(request)); String[] aridArr = request.getParameterValues("articelId"); if (aridArr != null&& aridArr.length > 0) { this.deleteArticle(aridArr); } } catch (Exception e) { model.setViewName(this.setExceptionRequest(request,e)); logger.error("AdminArticleController.delete()--error",e); } return model; }
順著變數的走向進行審計,articelId賦值給aridArr,而後進行了為空的判斷,不為空則執行deleteArticle操作,跟蹤定位此函數:
private void deleteArticle(String[]artidArr) { //刪除數據中記錄 articleService.deleteArticleByIds(artidArr); EHCacheUtil.remove(CacheConstans.ARTICLE_GOOD_RECOMMEND);
繼續跟蹤操作,articleService類的deleteArticleByIds函數,繼而進入DAO層,在ArticleDaoImpl.java內:
public void deleteArticleByIds(StringarticleIds) { this.delete("ArticleMapper.deleteArticleByIds",articleIds); }
進入ArticleMapper.xml,最終追蹤到deleteArticleByIds的SQL語句,使用了$拼接,典型的Mybatis注入:
<deleteid="deleteArticleByIds"parameterType="java.lang.String"> DELETEFROM EDU_ARTICLE WHERE EDU_ARTICLE.ARTICLE_ID IN (${value}) </delete>
除了順向思維,還可以通過逆向思維挖掘,pom.xml中看到系統使用的是Mybatis框架,可以直接去審查Maper.xml文件,查看是否使用$拼接:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>3.2.12.RELEASE</spring.version> <mybatis.version>3.2.7</mybatis.version> <aspectj.version>1.7.3</aspectj.version> <jdk.version>1.7</jdk.version> </properties>
2) Order by繞過預編譯
Webgoat一個order by的案例:

order by的參數orderExpression可以是一個selectExpression也可以是一個函數,比如使用一個case語句。
案例中可以根據IP或ID對數據進行排序:

對應程式碼為Server.java:
@GetMapping(produces =MediaType.APPLICATION_JSON_VALUE) @SneakyThrows @ResponseBody public List<Server> sort(@RequestParamString column) { Connection connection = DatabaseUtilities.getConnection(webSession); PreparedStatement preparedStatement =connection.prepareStatement("select id, hostname, ip, mac, status, descriptionfrom servers where status <> 'outof order' order by " + column); ResultSet rs = preparedStatement.executeQuery(); List<Server> servers = Lists.newArrayList(); while (rs.next()) { Server server = new Server(rs.getString(1), rs.getString(2),rs.getString(3), rs.getString(4), rs.getString(5), rs.getString(6)); servers.add(server); }
雖使用了預編譯但仍拼接了order by參數column,使用case探測語句探測:(case when (true) then id else ip end),如果真則以id排序,結果為:

3) 預編譯有誤
查看FAQ頁面數據功能getFaqPage函數,前端獲取page等參數:
publicFaqPageInfo getFaqPage(String tenantId, Map<String, String> conditions,int page, int pageSize, String like) { try { tenantId= WebUtil.getLoginTenantId(); FaqPageInfo fpi =FAQ_IO_SERVICE.getPageInfo(tenantId, conditions, page, pageSize, like); for (FaqModel fm : fpi.getData()) { fm.setWhitelistIds(WHITELIST_SERVICE.getWhiteListOnFaq(tenantId,fm.getId())); } return fpi; }
跟蹤FAQ_IO_SERVICE.getPageInfo,調用FaqSqlAccess.queryByPage進行處理:
public FaqPageInfo getPageInfo(StringtenantId, Map<String, String> conditions, int page, int pageSize, Stringlike) throws SQLException { FaqPageInfo pageInfo = new FaqPageInfo(); pageInfo.setData(FaqSqlAccess.queryByPage(tenantId, conditions, page,pageSize, like)); pageInfo.setTotalSize(FaqSqlAccess.queryCount(tenantId, conditions,like)); return pageInfo; }
找到FaqSqlAccess.java里的queryByPage方法,追蹤到SQL語句:
public static List<FaqModel>queryByPage(String tenantId, Map<String, String> conditions, int page,int pageSize, String like) throws SQLException { List<FaqModel> models = new ArrayList<>(); String language = conditions.get("language"); Connection connection = null; PreparedStatement stmt = null; try { connection = MysqlUtils.getConnection(); String sql = "select id, name, description, language, update_time,is_on from TOC_FAQ where tenant_id=?"; if (!CommonUtils.isEmptyStr(language)) sql += " andlanguage='" + language + "'"; if (!CommonUtils.isEmptyStr(like)) sql += " and name like'%" + like + "%'"; sql += " order by update_time desc limit ?,?"; stmt = connection.prepareStatement(sql); stmt.setString(1, tenantId); stmt.setInt(2, (page - 1) * pageSize); stmt.setInt(3, pageSize); ResultSet resultSet = stmt.executeQuery();
發現此處使用了預編譯,但language和like參數實際是直接拼接,存在SQL注入,對於getFaqPage功能構造參數"language":"'-if(substring(user(),1,1)=0x01,sleep(5),0)-'"}進行驗證。
簡單或複雜的SQL注入漏洞原理和審計方法相同,只是對於業務繁雜的系統,數據的走向和處理過程會比較複雜,調用鏈跟蹤難度會稍大一些,需要更多耐心。