提醒:本页面将不再更新、维护或者支持,文章、评论所叙述内容存在时效性,涉及技术细节或者软件使用方面不保证能够完全有效可操作,请谨慎参考!

单位某信息管理系统,需要加密狗才能运行,因为加密狗只有一个,该系统又需要维护各部门台账信息,遂思考在不增加加密狗的情况下能否将该系统分发下去,已知该系统采用Java Web方式编写,虽然有桌面应用的样子,但实质上是Web应用,很容易找到了Java Web服务器和实际的访问URL,在防火墙端口开放访问,并在路由器上做好端口映射,其他电脑访问正常,本来以为万事大吉,哪知道在登录的时候客户端提示没有加密狗无法登录。

审阅了登录页面的源代码,发现其采用ActiveX控件方式读取加密狗并设置登录信息以便于提交服务器,客户端没有加密狗,这个验证肯定也无法通过,原来我一直以为这个软件的加密狗是用于服务器端的,没想到客户端网页也采用了加密狗保护机制。

通常意义上加密狗是一种软件保护机制,主要防止软件被盗版,其通过硬件的方式保护核心的算法(比如关键公式),在软件需要的时候由硬件进行数据处理以完成整个软件流程,那么可以知道其登录信息的加密方式是存储在加密狗里面的,如果要登录成功又必须需要加密后的登录凭证。

因为该软件项目规模较大,反编译修改软件较为复杂,而且领导明确要求不可以改动原软件,所以只有另行他法。

已知我们现在有一正常工作的加密狗,为什么不直接借助该加密狗进行验证,再将验证数据代理提交呢?这里衍生出第一套方案,采用在有加密狗的服务器上私设反向代理Web服务器,并编写简单的登录页面,当用户使用该页面登录的时候我们获取到用户名和密码,再使用加密狗进行运算,最后将结果提交给真实的Java Web服务器,最后获取到Session会话的Cookie,再一次的将该Cookie使用Set-Cookie返回给用户,这样客户端用户相当于劫持了原来私设Web服务器与真实Java Web服务器的会话,然后成功登录。

反向代理劫持会话破解加密狗

这种方式要成功有个关键,那就是处于同一域名下(跨域名如果要成功主要要考虑Cookie跨站等因素),最好的状态是直接借助真实的Java Web服务器,设置新的登录页面,只不过将加密狗验证放在服务器端完成,这样可行性更高,也避免Cookie受到反XSS机制的干扰。

提到自己搭建Web服务器可能大家会想到使用IIS、Apache等,其实这里并不需要这么麻烦,因为我只需要维护一小部分用户访问,所以使用Python的SimpleHTTPServer库即可,对于向Web Server发出请求则用到了requests库(需要安装),一个简单实现的例子如下:

# -*- coding: utf-8 -*-

import cgi
import requests
import Cookie
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

class ProxyRobot(object):

    urls = {
        "login" : "/login.jsp",
        "auth"  : "/login.jsp",
    }
    
    base_url = "https://wangye.org/"
    
    headers = {
        "User-Agent"       : "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:15.0) Gecko/20090101 Firefox/14.0.1",
        "Accept"           : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language"  : "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3",
        "Accept-Encoding"  : "gzip,deflate",
        "Connection"       : "keep-alive",
        "Referer"          : "https://wangye.org/",
    }
    
    def post(self, url_name, data):
        et = self.session.post(ProxyRobot.base_url + ProxyRobot.urls[url_name], data, headers=ProxyRobot.headers)
        return et
        
    def get(self, url_name):
        et = self.session.get(ProxyRobot.base_url + ProxyRobot.urls[url_name], headers=ProxyRobot.headers)
        return et
        
    def __init__(self, username, password):
        self.session = requests.Session()
        self.credentials = {"username" : username, "password" : password}
        
    def _prepareLogin(self):
        request = self.get("login")
        # Modify something
        self.credentials["testcookie"] = 1
        return request.text
        
    def login(self):
        self._prepareLogin()
        payload = {
            "log" : self.credentials["username"],
            "pwd" : self.credentials["password"],
        }
        request = self.post("auth", data=payload)
        print request.text
        
    def getCookies(self):
        C = Cookie.SimpleCookie(self.session.cookies.get_dict())
        
        #cookies = self.session.cookies.get_dict()
        #for k,v in cookies.items():
        #    C[k] = v
            
        return C
        

class CustomHTTPRequestHandler(BaseHTTPRequestHandler):
  
    def getPostVars(self):
        ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
        if ctype == 'multipart/form-data':
            postvars = cgi.parse_multipart(self.rfile, pdict)
        elif ctype == 'application/x-www-form-urlencoded':
            length = int(self.headers.getheader('content-length'))
            postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1)
        else:
            postvars = {}
        return postvars
        
    def send_page_header(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        
    def send_redirect_header(self, url):
        self.send_response(302)
        self.send_header("Location", url)
        
    def do_POST(self):
        self.send_page_header()
        
        postvars = self.getPostVars()
        username = postvars.get("log", ['',])[0]
        password = postvars.get("pwd", ['',])[0]
        
        robot = ProxyRobot(username=username, password=password)
        robot.login()
        cookies = robot.getCookies()
        
        self.wfile.write(cookies.output())
        
        self.end_headers()
        
        
    def do_GET(self):
        self.send_page_header()
        self.end_headers()
        self.wfile.write("<html>\r\n<head><title>Login Gateway</title></head><body>\r\n")
        self.wfile.write("""
        <form action="" method="POST">
            Username: <input type="text" name="log" /> <br /><br />
            Password: <input type="password" name="pwd" /> <br /> <br />
            <input type="submit" value="Click to Login" />
        </form>
        """)
        self.wfile.write("\r\n</body></html>")


class CustomHTTPServer(HTTPServer):
    def __init__(self, host, port):
        HTTPServer.__init__(self, (host, port), CustomHTTPRequestHandler)


def main():
    server = CustomHTTPServer('127.0.0.1', 8000)
    server.serve_forever()


if __name__ == '__main__':
    main()

事实上我并没有采用上述的方法,因为不知道是什么原因Cookie经常获取不到,所以这里又提到了第二套方案,Plan B计划则采用稳妥方式,将验证交还给客户端,服务器端只保留加密狗验证功能,即接受客户端提交的用户名和密码由加密狗运算得到加密数据,将数据反馈给客户端,客户端再次持该加密数据提交给真实的Java Web服务器端进行验证,成功后就一路绿灯了,这种方式的稳定性和隐蔽性较高,对于Java Web服务器来说实际上仍然和真实的客户端进行数据交换。

服务器端代理绕过加密狗认证

对于Python代理服务器端调用加密狗的方式可以采用ActiveX,具体参考PyWin32的 Dispatch DispatchEx 相关。相关代码我就不贴出来了,这里有个小插曲,原来的软件不光登录页面采取了加密狗限制,甚至连登录后的页面都要验证加密狗,好在其登录后的页面主要加载了一个Flash(不得不吐槽一下该软件大部分由Flash构成),更奇葩的是其在每一次点击Flash子栏目的时候会发起一个检查加密狗是否正常的请求,要不是领导要求不能破坏软件,我都想直接屏蔽掉这个加密狗检查请求了,事实也证明是可以做到的,最后我把登录后的页面另存了一份修改页到我搭建的Python Web服务器下,然后加密狗数据请求部分再次使用Python调用服务器端加密狗获取,也算维护了整体软件的完整吧,具体的代码我就不贴了,以上思路仅供参考。