安全性“依赖客户端验证”的测试方法和代码实现

19 篇文章 2 订阅
16 篇文章 0 订阅

       “依赖客户端验证”顾名思义表达的是一种存在缺陷的片面性的对于客户端输入、提交数据的验证手段。典型B/S架构的客户端验证方式主要是依赖于浏览器端,通过解析HTML实现对输入框数字、字符长度限制;执行JavaScript脚本实现诸如RegularExpressionValidator、RequiredFieldValidator、CompareValidator、RangeValidator等验证器验证;利用Ajax与服务器的异步通信实现各类与数据库数据完整性以及需求相关的一系列验证等。这些验证的共同特点是一旦验证非法,浏览器便会阻止用户完成对非法数据的GET或POST,同时给出提示信息。

       首先需要肯定“依赖客户端验证”是有效的,使得易用性和可靠性得到了保证,而这些保证只限于正规使用用户。这对于安全性基本无效,渗透攻击会绕过客户端直接通过网络协议将非法数据发送到服务器,所以在安全性领域,最终还需要考察服务器端的验证能力。

       测试“依赖客户端验证”的方法即实现上述渗透攻击要达到的目的——直接通过网络协议将非法数据发送到服务器。这类攻击需要依靠工具,比如对于B/S架构的应用系统,工具除了需要实现HTTP协议通信外,还需要解决实际所面临的许多棘手问题,下文将会展示这些问题带来的麻烦和解决方法,从而实现一个普适的攻击程序用于测试 “依赖客户端验证”。

       另外要提的是,在实现所有这一切之前,你最好为自己挑选一个合适的HTTP客户端框架,以便快速地完成封装,实现必要的功能。本篇文章将采用Apache的HttpClient框架来进行开发,即便是如HttpClient框架如此灵活易用,但对于一般不常接触此类开发的开发人员来说,常常仅能够完成一些简单的HTTP GET或POST请求,本文将对利用HttpClient框架的一些技巧进行介绍,以便能够实现如LoadRunner或JMeter这类工具一样的复杂HTTP业务场景。

        问题一:首当其冲,我们在进行此类测试时,面对的第一个门槛便是登录页面,在进行身份鉴别时,往往采用SSL加密通道连接以便保证对敏感数据(密码信息)在网络传输中的安全保密性和数据完整性,因此,必须使HttpClient能够同时处理HTTP和HTTPS协议。有些网站采用的不是由正规安全机构签发的证书,或是证书已经过期,我们将通过继承ConnectionSocketFactory,实现一个绕过验证的SSL连接工厂类(实现证书认证加密的SSL连接工厂类可以参考HttpClient自带的例子源码ClientCustomSSL.java):

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpHost;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.protocol.HttpContext;

public class NoTrustSSLProtocolSocketFactory implements ConnectionSocketFactory{
	 private SSLContext sslcontext = null;   
     
     private SSLContext createSSLContext() {   
    	 SSLContext sslcontext=null;   
         try {   
             sslcontext = SSLContext.getInstance("SSL");   
             sslcontext.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());   
         } catch (NoSuchAlgorithmException e) {   
             e.printStackTrace();   
         } catch (KeyManagementException e) {   
             e.printStackTrace();
         }   
         return sslcontext;   
     }   
      
     private SSLContext getSSLContext() {   
         if (this.sslcontext == null) {   
             this.sslcontext = createSSLContext();   
         }   
         return this.sslcontext;   
     }   
     
     //自定义私有类 ,不实现证书和认证方法 
     private static class TrustAnyTrustManager implements X509TrustManager {   
         
         public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {   
         }   
     
         public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {   
         }   
     
         public X509Certificate[] getAcceptedIssuers() {   
             return new X509Certificate[]{};   
         }   
     }

     @Override
     public Socket connectSocket(int timeout, Socket socket, HttpHost target,
			InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext clientContext)
			throws IOException {
		if (clientContext == null) {   
           throw new IllegalArgumentException("null");   
       }   
       
       SocketFactory socketfactory = getSSLContext().getSocketFactory();   
       socket = socketfactory.createSocket();
       socket.bind(localAddress);   
       socket.connect(remoteAddress, timeout);   
       return socket;
	}

     @Override
     public Socket createSocket(HttpContext clientContext) throws IOException {
		// TODO Auto-generated method stub
		if (clientContext == null) {   
           throw new IllegalArgumentException("null");   
       }
		return getSSLContext().getSocketFactory().createSocket();
		
     }
}

       之后,将建立一个ConnectionSocketFactory类的注册器,该注册器分别注册在进行处理HTTP和HTTPS协议时所需要选择的ConnectionSocketFactory,对于HTTPS协议使用上面所创建的NoTrustSSLProtocolSocketFactory,对于HTTP协议使用框架自带的PlainConnectionSocketFactory,并实例化一个连接管理器:

Registry<ConnectionSocketFactory> reg = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("https", new NoTrustSSLProtocolSocketFactory())
                .register("http", new PlainConnectionSocketFactory())
                .build();
		
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);
       问题二:如何保持HTTP会话(Session或Cookie)状态?幸运的是HttpClient框架自带了解决方案,可以通过BasicCookieStore实现整个HttpClient生命周期内的状态保持,并可以灵巧的对其加以控制:

BasicCookieStore cookieStore = new BasicCookieStore();
       通过客户化方式注册初始化一个HttpClient对象,其中ConnectionManager和DefaultCookieStore选择我们上文所创建的reg和cookieStore对象:

CloseableHttpClient httpclient = HttpClients.custom()
                .setConnectionManager(cm).setDefaultCookieStore(cookieStore)
                .build();

       问题三:这类测试工具最棘手的问题是处理一些采用验证码方式以防止恶意攻击的页面,对于性能测试工具来说,最为直接的解决方式是利用一些图像识别算法进行直接辨识,虽然对于复杂的图形验证码识别率难以保证,但这是一类最优的解决方案;然而对于安全性测试工具来说,最为直接的解决方式是直接将图形验证码信息显示出来,进行人工干预验证,将图片资源下载到本地显示,并像处理正常网页逻辑一样进行处理:

ByteBuffer bb = ByteBuffer.allocateDirect(10240000);
HttpGet httpget = new HttpGet("https://localhost/vcode");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
       HttpEntity entity = response.getEntity();

       if (entity != null) {
               			 
              try {
    	       		    	 
    	             byte[] buffer = new byte[256];
    	       	     bb.clear();
    	       	     while(entity.getContent().read(buffer) != -1){
    	       	            bb.put(buffer);
    	             }
    	       		    bb.flip();
    	       		    buffer = new byte[bb.limit()];
    	      		    bb.get(buffer, 0, buffer.length);
    	      			
    	      			File file = new File("vcode.jpg");
    	      			FileOutputStream out = new FileOutputStream(file);
    	      			out.write(buffer);
    	      			out.flush();
    	      			out.close();
              } catch (IOException ex) {         
                   throw ex;
              }
       }
                     
       EntityUtils.consume(entity);
                    
} finally {
       response.close();
}
       然后通过图形界面显示,并适时弹出,实现人工干预处理,比如实现一个用于显示图形资源的JPanel:

import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JPanel;

public class ImageResourceShower extends JPanel{
	
	private static final long serialVersionUID = -1851712864293960374L;
	File file = null;
	public ImageResourceShower(File file){
		this.file = file;
	}
	@Override
	public void update(Graphics g){
		paint(g);
	}
	
	@Override
	public void paint(Graphics g) {
		
		BufferedImage image = null;
			try {
				image = ImageIO.read(file);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			g.drawImage(image,0,0,image.getWidth(),image.getHeight(),null);
			
		} 

}
       实现后的效果如下图所示:


       问题四:如何处理网页间的数据关联?一些成熟的性能测试工具为我们提供了参考,比如LoadRunner所提供的一系列关联函数如web_reg_save_param、web_reg_save_param_ex等,JMeter所提供的“正则表达式提取器”。我们同样可以实现一个通过设定左右边界正则表达式的方法实现关联功能:

public String string_param_save(String httpContent, String leftedge, String rightedge, int index){
		
		if(httpContent.toLowerCase().contains(leftedge.toLowerCase()))
		{
			ArrayList<String> paramList = new ArrayList<String>(2);
			Pattern pa = Pattern.compile(Pattern.quote(leftedge) + "(.*?)" + Pattern.quote(rightedge),Pattern.CASE_INSENSITIVE);
			Matcher ma = pa.matcher(httpContent);
			while(ma.find()){
				
				paramList.add(ma.group(1));
			}
			return paramList.get(index);
		} else {
			return null;
		}
}
       问题五:有时会碰到一些浏览器端用于计算的JavaScript脚本需要进行处理,因为计算结果会被发送的服务器,如这样的一个GET方法:/vcode?'+Math.random()*100。处理JavaScript脚本需要借助一些成熟完善的JavaScript引擎,比如著名的V8引擎或Rhino引擎,以下给出以V8引擎为例的一些Java解析执行JavaScript脚本的方法代码:

import javax.script.ScriptEngineManager;

public class JavaScriptRunner {
	ScriptEngineManager factory = new ScriptEngineManager();
	ScriptEngine engine = factory.getEngineByName("jav8");
	
	//Java调用javascript中定义的方法
	public void jsInvoke(){
		Invocable invocable = (Invocable) this.engine; 
    	
    	try {
			this.engine.eval("function hello(name) { return 'Hello ' + name; };" +
						  "function person(name) {" +
						  "	var say = function (name) {" +
						  "    return 'Hello';}();" +
						  " return say + ' ' + name;"+
						  "};");
			System.out.println(invocable.invokeFunction("hello", "xreztento"));
			System.out.println(invocable.invokeFunction("person", "xreztento"));
		} catch (ScriptException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	//Java调用javascript中的一段执行代码
	public void jsCompile(){
		Compilable compiler = (Compilable)engine;
		try {
			CompiledScript script = compiler.compile("Math.random()*100");
			System.out.println(script.eval());
			
		} catch (ScriptException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

       问题六:遇到302重定向时,处理方法是从HTTP Response Header中取得属性为Location的值,再将此值输入到下一个HttpGet对象中,取出Location值的主要方法如下:

String location = "";
if(response.getStatusLine().getStatusCode() == 302){
                     for(Header header:response.getAllHeaders()){
                         System.out.println(header.getName() + ":" +header.getValue());
                         if(header.getName().equals("Location")){
                             location = header.getValue();
                         }
                     }
                 }
       当上面的问题全部得到解决后,我们便可以在遇到各类问题时将其解决方案代码穿插在各种GET和POST片段代码的中间,最终组合成为一个用于“依赖客户端验证”测试的安全性测试工具。下面给出一个模板化的GET和POST片段代码

(1)GET方法片段代码:

             HttpGet httpget = new HttpGet("http://localhost");
             CloseableHttpResponse response = httpclient.execute(httpget);
             try {
                 HttpEntity entity = response1.getEntity();
                 StringBuffer sb = new StringBuffer(1024000);
                 byte[] buffer = new byte[256];
                 while(entity.getContent().read(buffer) != -1){
                	 sb.append(new String(buffer,"utf-8"));
                 }
                 EntityUtils.consume(entity);
             } finally {
                 response.close();
             }

(2)POST方法片段代码:

HttpUriRequest login = RequestBuilder.post()
                     .setUri(new URI("http://localhost"))
                     .addParameter("username", "xreztento")
                     .addParameter("password", "password")
                     .build();
                     
            
             CloseableHttpResponse response = httpclient.execute(login);
             try {
                 HttpEntity entity = response.getEntity();
                 StringBuffer sb = new StringBuffer(1024000);
                 byte[] buffer = new byte[256];
                 while(entity.getContent().read(buffer) != -1){
                	 sb.append(new String(buffer,"utf-8"));
                 }
                 System.out.println(sb.toString());
                 EntityUtils.consume(entity);
             } finally {
                 response.close();
             }
       尾声:从笔者的工作生涯来看,“依赖客户端验证”的网站应用比比皆是,这些应用潜藏着巨大的安全隐患,本文所介绍的各种问题的解决方案以及对于HttpClient框架的使用不仅适用于安全性测试,如果将其置于一个多线程框架再结合一些计数器代码完全可以实现对Web/HTTP应用复杂场景的性能测试,不仅如此,再进一步封装,便可以实现一个轻量级的通用型安全性或性能测试工具,那就要看如何好好的利用。另外,本文中所涉及的代码只是用来学习交流,请不要用于渗透攻击等行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值