a) 文件上传功能。这里说的文件上传在很多功能点都会出现,比如像文章编辑、资料编辑、头像上传、附件上传,这个功能最常见的漏洞就是任意文件上传了,后端程序没有严格地限制上传文件的格式,导致可以直接上传或者存在绕过的情况,而除了文件上传功能外,还经常发生SQL注入漏洞。因为一般程序员都不会注意到对文件名进行过滤,但是又需要把文件名保存到数据库中,所以就会存在SQL注入漏洞。
b) 文件管理功能。在文件管理功能中,如果程序将文件名或者文件路径直接在参数中传递,则很有可能会存在任意文件操作的漏洞,比如任意文件读取等,利用的方式是在路径中使用../或者..\跳转目录。
c) 登录认证功能。登录认证功能不是指一个登录过程,而是整个操作过程中的认证,目前的认证方式大多是基于Cookie和Session,不少程序会把当前登录的用户账号等认证信息放到Cookie中,或许是加密方式,是为了保持用户可以长时间登录,不会一退出浏览器或者Session超时就退出账户,进行操作的时候直接从Cookie中读取出当前用户信息,这里就存在一个算法可信的问题,如果这段Cookie信息没有加salt一类的东西,就可以导致任意用户登录漏洞,只要知道用户的部分信息,即可生成认证令牌,甚至有的程序会直接把用户名明文放到Cookie中,操作的时候直接取这个用户名的数据,这也是常说的越权漏洞。
Statement stmt =null;ResultSet rs =null;try{String userName =ctx.getAuthenticatedUserName(); //this is a constantString sqlString ="SELECT * FROM t_item WHERE owner='"+ userName +"' AND itemName='"+request.getParameter("itemName") +"'"; stmt =connection.createStatement(); rs =stmt.executeQuery(sqlString);// ... result set handling}catch (SQLException se){// ... logging and error handling}
PreparedStatement stmt =nullResultSet rs =nulltry{String userName =ctx.getAuthenticatedUserName(); //this is a constantString itemName =request.getParameter("itemName");// ...Ensure that the length of userName and itemName is legitimate// ...String sqlString ="SELECT * FROM t_item WHERE owner=? AND itemName=?"; stmt =connection.prepareStatement(sqlString);stmt.setString(1, userName);stmt.setString(2, itemName); rs =stmt.executeQuery();// ... result set handling}catch (SQLException se){// ... logging and error handling}
如果使用参数化查询,则在SQL语句中使用占位符表示需在运行时确定的参数值。参数化查询使得SQL查询的语义逻辑被预先定义,而实际的查询参数值则等到程序运行时再确定。参数化查询使得数据库能够区分SQL语句中语义逻辑和数据参数,以确保用户输入无法改变预期的SQL查询语义逻辑。在Java中,可以使用java.sql.PreparedStatement来对数据库发起参数化查询。在这个正确示例中,如果一个攻击者将itemName输入为name' OR 'a' = 'a,这个参数化查询将免受攻击,而是会查找一个itemName匹配name' OR 'a' = 'a这个字符串的条目。
错误示例(在存储过程中动态构建SQL):
Java代码:
CallableStatement =nullResultSet results =null;try{String userName =ctx.getAuthenticatedUserName(); //this is a constantString itemName =request.getParameter("itemName"); cs =connection.prepareCall("{call sp_queryItem(?,?)}");cs.setString(1, userName);cs.setString(2, itemName); results =cs.executeQuery();// ... result set handling}catch (SQLException se){// ... logging and error handling}
SQL Server存储过程:
CREATEPROCEDURE sp_queryItem @userNamevarchar(50), @itemNamevarchar(50)ASBEGIN DECLARE @sqlnvarchar(500); SET @sql='SELECT * FROM t_item WHERE owner = '''+ @userName+''' AND itemName = '''+ @itemName+'''';EXEC(@sql); ENDGO
CallableStatement =nullResultSet results =null;try{String userName =ctx.getAuthenticatedUserName(); //this is a constantString itemName =request.getParameter("itemName");// ... Ensure that the length of userName and itemName is legitimate// ... cs =connection.prepareCall("{call sp_queryItem(?,?)}");cs.setString(1, userName);cs.setString(2, itemName); results =cs.executeQuery();// ... result set handling}catch (SQLException se){// ... logging and error handling}
<select id="getItems" parameterClass="MyClass" resultClass="Item"> SELECT *FROM t_item WHERE owner = #userName# AND itemName = #itemName#</select>#符号括起来的userName和itemName两个参数指示iBATIS在创建参数化查询时将它们替换成占位符:String sqlString ="SELECT * FROM t_item WHERE owner=? AND itemName=?";PreparedStatement stmt =connection.prepareStatement(sqlString);stmt.setString(1,myClassObj.getUserName());stmt.setString(2,myClassObj.getItemName());ResultSet rs =stmt.executeQuery();// ... convert results set to Item objects
然而,iBATIS也允许使用$符号指示使用某个参数来直接拼接SQL语句,这种做法是有SQL注入漏洞的:(order by 只能用$,用#{}会多个' '导致sql语句失效.此外还有一个like 语句后也需要用${},这俩语句需要单独对传入参数做过滤)
<select id="getItems" parameterClass="MyClass" resultClass="items"> SELECT *FROM t_item WHERE owner = #userName# AND itemName ='$itemName$'</select>
iBATIS将会为以上SQL映射执行类似下面的代码:
String sqlString ="SELECT * FROM t_item WHERE owner=? AND itemName='"+myClassObj.getItemName() +"'";PreparedStatement stmt =connection.prepareStatement(sqlString);stmt.setString(1,myClassObj.getUserName());ResultSet rs =stmt.executeQuery();// ... convert results set to Item objects
在这里,攻击者可以利用itemName参数发起SQL注入攻击。
正确示例(对不可信输入做校验):
publicList<Book>queryBooks(List<Expression> queryCondition){/* ... */try {StringBuilder sb =newStringBuilder("select * from t_book where ");Codec oe =newOracleCodec();if (queryCondition !=null&&!queryCondition.isEmpty()) {for (Expression e : queryCondition) {String exprString =e.getColumn() +e.getOperator() +e.getValue();String safeExpr =ESAPI.encoder().encodeForSQL(oe, exprString);sb.append(safeExpr).append(" and "); }sb.append("1=1");Statement stat =connection.createStatement();ResultSet rs =stat.executeQuery(sb.toString());//other omitted code } }/* ... */}
<user>
<id>joe</id>
<role>Administrator</role><!--</id> <role>operator</role> <description> -->
<description>I want to be an administrator</description>
</user>
<user>
<id>joe</id><role>Administrator</role><!—</id>
<role>operator</role>
<description>--><description>I want to be an administrator</description>
</user>
说明:如果在记录的日志中包含未经校验的不可信数据,则可能导致日志注入漏洞。恶意用户会插入伪造的日志数据,从而让系统管理员误以为这些日志数据是由系统记录的。例如,一个用户可能通过输入一个回车符和一个换行符(CRLF)序列来将一条合法日志拆分成两条日志,其中每一条都可能会令人误解。将未经净化的用户输入写入日志还可能会导致向信任边界之外泄露敏感数据,或者导致违反当地法律法规,在日志中写入和存储了某些类型的敏感数据。例如,如果一个用户要把一个未经加密的信用卡号插入到日志文件中,那么系统就会违反了PCI DSS(Payment Card Industry Data Security Standard)标准。可以通过验证和净化发送到日志的任何不可信数据来防止日志注入攻击。
// ...volatileboolean validFlag =false;do{try {// If requested file does not exist, throws FileNotFoundException// If requested file exists, sets validFlag to true validFlag =true; }catch (FileNotFoundException e) {// Ask the user for a different file name }} while (validFlag !=true);// Use the file
publicinterfaceReporter{publicvoidreport(Throwable t);}publicclassExceptionReporter{// Exception reporter that prints the exception // to the console (used as default)privatestaticfinalReporter printException =newReporter() {publicvoidreport(Throwable t) {System.err.println(t.toString()); } };// Stores the default reporter.// The default reporter can be changed by the user.privatestaticReporter default = printException;// Helps change the default reporter back to // PrintException in the futurepublicstaticReportergetPrintException() {return printException; }publicstaticReportergetExceptionReporter() {returndefault; }// May throw a SecurityException (which is unchecked)publicstaticvoidsetExceptionReporter(Reporter reporter) {// Custom permissionExceptionReporterPermission perm =newExceptionReporterPermission("exc.reporter");SecurityManager sm =System.getSecurityManager();if (sm !=null) {// Check whether the caller has appropriate permissionssm.checkPermission(perm); }// Change the default exception reporterdefault= reporter; }}
classMyExceptionReporterextendsExceptionReporter{publicstaticvoidreport(Throwable t) { t =filter(t);// Do any necessary user reporting (show dialog box or send to console) }publicstaticExceptionfilter(Throwable t) {// Sanitize sensitive data or replace sensitive exceptions with non-sensitive exceptions (whitelist)// Return non-sensitive exception }}
publicclassExceptionExample{publicstaticvoidmain(String[] args) throwsFileNotFoundException {// Linux stores a user's home directory path in// the environment variable $HOME, Windows in %APPDATA%// ... Other omitted codeFileInputStream fis =newFileInputStream(System.getenv("APPDATA")+ args[0]); } }
内,就把文件保存在服务器上,导致恶意用户可以上传任意文件,甚至上传脚本木马到 web 服务器上,直接控制 web 服务器。
错误示例:
PrintWriter pw =newPrintWriter(new BufferedWriter(new FileWriter( request.getRealPath("/")+getFIlename(request))));ServletInputStream in =request.getInputStream();int i =in.read();while (i !=-1) {pw.print((char) i);i =in.read();}pw.close();
解决方案:处理用户上传文件,要做以下检查:
1、 检查上传文件扩展名白名单,不属于白名单内,不允许上传。
2、 上传文件的目录必须是 http 请求无法直接访问到的。如果需要访问的,必须上传到其他(和 web 服务器不同的)域名下,并设置该目录为不解析 jsp 等脚本语言的目录。
publicstaticfinalint BUFFER =512;publicstaticfinalint TOOBIG =0x6400000; // 100MB// ...publicfinalvoidunzip(String filename) throws java.io.IOException{FileInputStream fis =newFileInputStream(filename);ZipInputStream zis =newZipInputStream(new BufferedInputStream(fis));ZipEntry entry;try {while ((entry =zis.getNextEntry()) !=null) {System.out.println("Extracting: "+ entry);int count;byte data[] =newbyte[BUFFER];// Write the files to the disk, but only if the file is not insanely bigif (entry.getSize() > TOOBIG) {thrownewIllegalStateException("File to be unzipped is huge."); }if (entry.getSize() ==-1) {thrownewIllegalStateException("File to be unzipped might be huge."); }FileOutputStream fos =newFileOutputStream(entry.getName());BufferedOutputStream dest =newBufferedOutputStream(fos, BUFFER);while ((count =zis.read(data,0, BUFFER)) !=-1) {dest.write(data,0, count); }dest.flush();dest.close();zis.closeEntry(); } }finally {zis.close(); }}
staticfinalint BUFFER =512;staticfinalint TOOBIG =0x6400000; // max size of unzipped data, 100MBstaticfinalint TOOMANY =1024; // max number of files// ...privateStringsanitzeFileName(String entryName,String intendedDir) throws IOException{File f =newFile(intendedDir, entryName);String canonicalPath =f.getCanonicalPath();File iD =newFile(intendedDir);String canonicalID =iD.getCanonicalPath();if (canonicalPath.startsWith(canonicalID)) {return canonicalPath; }else {thrownewIllegalStateException("File is outside extraction target directory."); }}// ...publicfinalvoidunzip(String fileName) throws java.io.IOException{FileInputStream fis =newFileInputStream(fileName);ZipInputStream zis =newZipInputStream(new BufferedInputStream(fis));ZipEntry entry;int entries =0;int total =0;byte[] data =newbyte[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 bigString name =sanitzeFileName(entry.getName(),".");FileOutputStream fos =newFileOutputStream(name);BufferedOutputStream dest =newBufferedOutputStream(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) {thrownewIllegalStateException("Too many files to unzip."); }if (total > TOOBIG) {thrownewIllegalStateException("File being unzipped is too big."); } } }finally {zis.close(); }}
publicclassGPSLocationimplementsSerializable{privatetransientdouble x; // transient field will not be serialized privatetransientdouble y; // transient field will not be serialized privateString id;// other content}
publicclassGPSLocationimplementsSerializable{privatedouble x;privatedouble y;privateString id;// sensitive fields x and y are not content in serialPersistentFieldsprivatestaticfinalObjectStreamField[] serialPersistentFields = {newObjectStreamField("id",String.class)};// other content}
publicfinalclassHometownimplementsSerializable{privatestaticfinallong serialVersionUID =9078808681344666097L;// Private internal state privateString town;privatestaticfinalString UNKNOWN ="UNKNOWN";voidperformSecurityManagerCheck() throwsSecurityException {// verify whether current user has rights to access the file }voidvalidateInput(String newCC) throwsInvalidInputException {// ... }publicHometown() {performSecurityManagerCheck();// Initialize town to default value town = UNKNOWN; }// Allows callers to retrieve internal stateStringgetValue() {performSecurityManagerCheck();return town; }// Allows callers to modify (private) internal statepublicvoidchangeTown(String newTown) throwsInvalidInputException {if (town.equals(newTown)) {// No changereturn; }else {performSecurityManagerCheck();validateInput(newTown); town = newTown; } }privatevoidwriteObject(ObjectOutputStream out) throwsIOException {out.writeObject(town); }privatevoidreadObject(ObjectInputStream in) throwsIOException,ClassNotFoundException {in.defaultReadObject();// If the deserialized name does not match// the default value normally// created at construction time, duplicate the checksif (!UNKNOWN.equals(town)) {validateInput(town); } }}
publicfinalclassHometownimplementsSerializable{// ... all methods the same except the following: // writeObject() correctly enforces checks during serialization privatevoidwriteObject(ObjectOutputStream out) throwsIOException {performSecurityManagerCheck();out.writeObject(town); }// readObject() correctly enforces checks during deserialization privatevoidreadObject(ObjectInputStream in) throwsIOException,ClassNotFoundException {in.defaultReadObject();// If the deserialized name does not match the default value normally // created at construction time, duplicate the checks if (!UNKNOWN.equals(town)) {performSecurityManagerCheck();validateInput(town); } }}
publicclassIPaddress{publicstaticvoidmain(String[] args) throwsIOException {char[] ipAddress =newchar[100];BufferedReader br =newBufferedReader(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(); } }
Java API 提供了伪随机数生成器(PRNG)—— java.util.Random类。这个伪随机数生成器具有可移植性和可重复性。因此,如果两个java.util.Random类的实例创建时使用的是相同的种子值,那么对于所有的Java实现,它们将生成相同的数字序列。在系统重启或应用程序初始化时,Seed值总是被重复使用。在一些其他情况下,seed值来自系统时钟的当前时间。攻击者可以在系统的一些安全脆弱点上监听,并构建相应的查询表预测将要使用的seed值。
//Exception handling has been omitted for the sake of brevityclassEchoServer{publicstaticvoidmain(String[] args) throwsIOException {//Exception handling has been omitted for the sake of brevity//...ServerSocket serverSocket =newServerSocket(9999);Socket socket =serverSocket.accept();PrintWriter out =newPrintWriter(socket.getOutputStream(),true);BufferedReader in =newBufferedReader(new InputStreamReader(socket.getInputStream()));String inputLine;while ((inputLine =in.readLine()) !=null) {System.out.println(inputLine);out.println(inputLine); }// ... }// ...}classEchoClient{publicstaticvoidmain(String[] args) throwsUnknownHostException,IOException {// Exception handling has been omitted for the sake of brevity// ...Socket socket =newSocket(getServerIp(),9999);PrintWriter out =newPrintWriter(socket.getOutputStream(),true);BufferedReader in =newBufferedReader(new InputStreamReader(socket.getInputStream()));BufferedReader stdIn =newBufferedReader(new InputStreamReader(System.in));String userInput;while ((userInput =stdIn.readLine()) !=null) {out.println(userInput);System.out.println(in.readLine()); }// ... }// ...}
正确示例:
classEchoServer{publicstaticvoidmain(String[] args) throwsIOException {// Exception handling has been omitted for the sake of brevity// ...SSLServerSocketSSLServerSocketFactory sslServerSocketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(9999);SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();PrintWriter out =newPrintWriter(sslSocket.getOutputStream(),true);BufferedReader in =newBufferedReader(new InputStreamReader(sslSocket.getInputStream()));String inputLine;while ((inputLine =in.readLine()) !=null) {System.out.println(inputLine);out.println(inputLine); }// ... }// ...}classEchoClient{publicstaticvoidmain(String[] args) throwsIOException {// Exception handling has been omitted for the sake of brevity// ...SSLSocketSSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); sslSocket = (SSLSocket) sslSocketFactory.createSocket(getServerIp(),9999);PrintWriter out =newPrintWriter(sslSocket.getOutputStream(),true);BufferedReader in =newBufferedReader(new InputStreamReader(sslSocket.getInputStream()));BufferedReader stdIn =newBufferedReader(new InputStreamReader(System.in));String userInput;while ((userInput =stdIn.readLine()) !=null) {out.println(userInput);System.out.println(in.readLine()); }// ... }// ...}
Dependency-Check是OWASP(Open Web Application Security Project)的一个实用开源程序,用于识别项目依赖项并检查是否存在任何已知的,公开披露的漏洞。目前,已支持Java、.NET、Ruby、Node.js、Python等语言编写的程序,并为C/C++构建系统(autoconf和cmake)提供了有限的支持。而且该工具还是OWASP Top 10的解决方案的一部分。