a) 文件上传功能。这里说的文件上传在很多功能点都会出现,比如像文章编辑、资料编辑、头像上传、附件上传,这个功能最常见的漏洞就是任意文件上传了,后端程序没有严格地限制上传文件的格式,导致可以直接上传或者存在绕过的情况,而除了文件上传功能外,还经常发生SQL注入漏洞。因为一般程序员都不会注意到对文件名进行过滤,但是又需要把文件名保存到数据库中,所以就会存在SQL注入漏洞。
b) 文件管理功能。在文件管理功能中,如果程序将文件名或者文件路径直接在参数中传递,则很有可能会存在任意文件操作的漏洞,比如任意文件读取等,利用的方式是在路径中使用../或者..\跳转目录。
c) 登录认证功能。登录认证功能不是指一个登录过程,而是整个操作过程中的认证,目前的认证方式大多是基于Cookie和Session,不少程序会把当前登录的用户账号等认证信息放到Cookie中,或许是加密方式,是为了保持用户可以长时间登录,不会一退出浏览器或者Session超时就退出账户,进行操作的时候直接从Cookie中读取出当前用户信息,这里就存在一个算法可信的问题,如果这段Cookie信息没有加salt一类的东西,就可以导致任意用户登录漏洞,只要知道用户的部分信息,即可生成认证令牌,甚至有的程序会直接把用户名明文放到Cookie中,操作的时候直接取这个用户名的数据,这也是常说的越权漏洞。
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; }}
class MyExceptionReporter extends ExceptionReporter
{
public static void report(Throwable t)
{
t = filter(t);
// Do any necessary user reporting (show dialog box or send to console)
}
public static Exception filter(Throwable t)
{
// Sanitize sensitive data or replace sensitive exceptions with non-sensitive exceptions (whitelist)
// Return non-sensitive exception
}
}
public class ExceptionExample
{
public static void main(String[] args) throws FileNotFoundException
{
// Linux stores a user's home directory path in
// the environment variable $HOME, Windows in %APPDATA%
// ... Other omitted code
FileInputStream fis = new FileInputStream(System.getenv("APPDATA")
+ args[0]);
}
}
内,就把文件保存在服务器上,导致恶意用户可以上传任意文件,甚至上传脚本木马到 web 服务器上,直接控制 web 服务器。
错误示例:
PrintWriter pw = new PrintWriter(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 等脚本语言的目录。
public static void main(String[] args)
{
File f = new File(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String absPath = f.getAbsolutePath();
if (!isInSecureDir(Paths.get(absPath)))
{
// Refer to Rule 3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if (!validate(absPath))
{
// Validation
throw new IllegalArgumentException();
}
/* … */
}
public static void main(String[] args) throws IOException
{
File f = new File(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String canonicalPath = f.getCanonicalPath();
if (!isInSecureDir(Paths.get(absPath)))
{
// Refer to Rule 3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if (!validate(absPath))
{
// Validation
throw new IllegalArgumentException();
}
/* ... */
}
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();
}
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();
}
}
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();
}
}
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
}
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
}
public final class Hometown implements Serializable
{
private static final long serialVersionUID = 9078808681344666097L;
// Private internal state
private String town;
private static final String UNKNOWN = "UNKNOWN";
void performSecurityManagerCheck() throws SecurityException
{
// verify whether current user has rights to access the file
}
void validateInput(String newCC) throws InvalidInputException
{
// ...
}
public Hometown()
{
performSecurityManagerCheck();
// Initialize town to default value
town = UNKNOWN;
}
// Allows callers to retrieve internal state
String getValue()
{
performSecurityManagerCheck();
return town;
}
// Allows callers to modify (private) internal state
public void changeTown(String newTown) throws InvalidInputException
{
if (town.equals(newTown))
{
// No change
return;
}
else
{
performSecurityManagerCheck();
validateInput(newTown);
town = newTown;
}
}
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeObject(town);
}
private void readObject(ObjectInputStream in) throws IOException, 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))
{
validateInput(town);
}
}
}
public final class Hometown implements Serializable
{
// ... all methods the same except the following:
// writeObject() correctly enforces checks during serialization
private void writeObject(ObjectOutputStream out) throws IOException
{
performSecurityManagerCheck();
out.writeObject(town);
}
// readObject() correctly enforces checks during deserialization
private void readObject(ObjectInputStream in) throws IOException, 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);
}
}
}
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();
}
}
Java API 提供了伪随机数生成器(PRNG)—— java.util.Random类。这个伪随机数生成器具有可移植性和可重复性。因此,如果两个java.util.Random类的实例创建时使用的是相同的种子值,那么对于所有的Java实现,它们将生成相同的数字序列。在系统重启或应用程序初始化时,Seed值总是被重复使用。在一些其他情况下,seed值来自系统时钟的当前时间。攻击者可以在系统的一些安全脆弱点上监听,并构建相应的查询表预测将要使用的seed值。
//Exception handling has been omitted for the sake of brevity
class EchoServer
{
public static void main(String[] args) throws IOException
{
//Exception handling has been omitted for the sake of brevity
//...
ServerSocket serverSocket = new ServerSocket(9999);
Socket socket = serverSocket.accept();
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
{
System.out.println(inputLine);
out.println(inputLine);
}
// ...
}
// ...
}
class EchoClient
{
public static void main(String[] args) throws UnknownHostException,
IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
Socket socket = new Socket(getServerIp(), 9999);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(
System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null)
{
out.println(userInput);
System.out.println(in.readLine());
}
// ...
}
// ...
}
正确示例:
class EchoServer
{
public static void main(String[] args) throws IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
SSLServerSocket SSLServerSocketFactory sslServerSocketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(9999);
SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
PrintWriter out = new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
{
System.out.println(inputLine);
out.println(inputLine);
}
// ...
}
// ...
}
class EchoClient
{
public static void main(String[] args) throws IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
SSLSocket SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
sslSocket = (SSLSocket) sslSocketFactory.createSocket(getServerIp(),
9999);
PrintWriter out = new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
BufferedReader stdIn = new BufferedReader(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的解决方案的一部分。