C++网络安全工具编程(二)Web目录扫描器

我们知道对目标网站的访问是基于Http协议的,而Http又是基于TCPsocket连接的,所以本例中将会使用到上一节中的部分代码,即关于C++最简socket连接建立的Demo。
建立socket连接的过程请参考《C++网络安全工具编程(一)connect端口扫描器》中的基础部分。

当成功与目标服务器的80端口(即http服务端口)建立连接后,我们即可通过发送请求和接收消息来与目标网站服务器进行信息交互,大部分浏览器的实现也是基于此原理。

那么,应该怎样与目标网页服务器进行交互呢。这里我们需要先了解一下Http报文。

先来看下Http请求报文


(图来自网络)

首先如图
Http报文包括了:请求行、请求头部、空行、请求数据
(1)请求行
请求行由请求方法字段、URL字段和HTTP协议版本字段组成,用空格分隔。如,GET /index.html HTTP/1.1
HTTP协议的请求方法最常用的GET方法和POST方法。

GET方法:
GET方法要求服务器将URL定位的资源放在响应报文的数据部分,回送给客户端。使用GET方法时,请求参数和对应的值附加在URL后面,利用一个问号(“?”)代表URL的结尾与请求参数的开始,GET方法为显性传参,通常用于传递非安全需要的信息。
例如:/index.php?id=1
GET方法最多只能像服务器发送1024byte的数据,同时GET报文没有报文体的,只有头部,数据在GET url?xxx=xx中传出

POST:
当客户端给服务器提供信息较多时可以使用POST方法。此方法为隐性传参,通常用于帐号密码的传递。
POST方法将请求参数封装在HTTP请求数据中,以名称/值的形式出现,可以传输大量数据,可用来传送文件。

(2)请求头部
请求头部由Key/Value对组成。请求头部用于通知服务器有关于客户端请求的信息:
User-Agent:客户端浏览器类型。
Accept:客户端内容类型列表。
Host:目标主机IP。

(3)空行
空行用于发送回车符和换行符,通知服务器请求头完毕。
若不发送空行,服务器会认为本次请求的数据尚未完全发送,会处于等待状态。

(4)请求数据
这是在POST方法中独有的。POST常用于表单提交,通常方法有Content-Type和Content-Length。

我们来分别看一下POST方法以及GET方法的Http请求报文

POST报文头:
POST index.php HTTP/1.1
Accept: */*
Accept-Language: zh-cn
host: www.beitown.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 12
Connection:close //连接方式
//空行 \r\n\r\n
name = beitown//发送的数据 (加在空行之后)

GET报文头:
GET index.php?name=beitown HTTP/1.1
Accept: */*
Accept-Language: zh-cn
host: www.beitown.com
Connection:Keep-Alive //连接方式

以下代码将使用GET方法发送一个Http头请求,并获取网页服务器返回的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include "stdafx.h"
#include <iostream>
#include <Winsock2.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
int _tmain(int argc, _TCHAR* argv[])
{
    //初始化Windows Sockets 动态库
    WSADATA wsaData;
    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
    {
        cout<<"找不到可使用的WinSock dll!"<<endl;
        return 1;
    }
    //创建套接字
    SOCKET sClient=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(sClient==INVALID_SOCKET)
    {
        cout<<"创建客户端socket失败!"<<endl;
        return 1;
    }
    //连接服务器
    SOCKADDR_IN addrServ;
    addrServ.sin_family=AF_INET;
    addrServ.sin_addr.S_un.S_addr=inet_addr("74.53.53.200");
    addrServ.sin_port=htons(80);
    if(connect(sClient,(sockaddr *)&addrServ,sizeof(sockaddr))==SOCKET_ERROR)
    {
        cout<<"连接服务器失败!"<<endl;
        closesocket(sClient);
        return 1;
    }
    else
        cout<<"连接服务器成功!"<<endl;
    char  * content = "index.php";  
    string str="";
    str.append("GET /");
    str.append(content);
    str.append(" HTTP/1.1");
    str.append("\r\nHost:");
    str.append("www.beitown.com");
    str.append("\r\nAccept:*/*");
    str.append("\r\nConnection:Keep-Alive");
    str.append("\r\n\r\n");

    //发送数据
    int retval=send(sClient,str.data(),str.length()+1,0);
    if(retval==SOCKET_ERROR)
    {
        cout<<"发送数据失败!"<<endl;
    }
    //接收数据
    char buf[1024];
    memset(buf,0,1024);
    retval=recv(sClient,buf,1024,0);
    if(retval==SOCKET_ERROR)
    {
        cout<<"接收数据失败!"<<endl;
    }
    cout<<buf<<endl;
    getchar();
    //关闭套接字,释放资源
    closesocket(sClient);
    WSACleanup();
    return 0;
}

通过对目标网页服务器80端口建立的socket连接,发送GET请求,服务器获取GET请求后返回HTTP响应头,若网页可以访问则在相应头后还会发送网页HTML编码
如图

服务器返回的Http响应头格式如下图

(本图来自网络)

Http响应头大致分为如下几种
1xx:信息响应类,表示接收到请求并且继续处理
2xx:处理成功响应类,表示动作被成功接收、理解和接受
3xx:重定向响应类,为了完成指定的动作,必须接受进一步处理
4xx:客户端错误,客户请求包含语法错误或者是不能正确执行
5xx:服务端错误,服务器不能正确执行一个正确的请求

Demo中收到200 OK表示网页可以被成功访问。

以上即完成了一个最简的C++访问目标WEB页的过程。
接下来我们将此Demo进一步拓展,制作一个自己的网页目录扫描器,又称网站挖掘机、目录爆破器。

对网站目录的扫描其原理就是以穷举猜测常用目录名的方式进行爆破,这里就需要使用到一个字典,通常用txt格式即可。程序读入字典里实现写好的常用目录路径,并依次进行试探性扫描。读入txt文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
    fstream file;
    file.open("dir.txt",ios::in);

    if(!file)
        cout<<"file not founded"<<endl;

    string str;  
    while( getline(file,str) )
    {    
        cout << "Read from file: " << str << endl;
    }
    file.close();

当程序目录下有dir.txt文件时,程序将逐行读取txt文件中的数据并存入字符串str中。
整合代码,最终程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "stdafx.h"
#include <iostream>
#include <Winsock2.h>
#include <fstream>
#include <string>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
int _tmain(int argc, _TCHAR* argv[])
{
    //初始化Windows Sockets 动态库
    WSADATA wsaData;
    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
    {
        cout<<"找不到可使用的WinSock dll!"<<endl;
        return 1;
    }
    SOCKADDR_IN addrServ;
    addrServ.sin_family=AF_INET;
    addrServ.sin_addr.S_un.S_addr=inet_addr("74.53.53.200");
    addrServ.sin_port=htons(80);

    SOCKET sClient;
        //打开字典文件
    fstream file;
    file.open("dir.txt",ios::in);
    if(!file)
        cout<<"file not founded"<<endl;
    string dir;  
    while( getline(file,dir) )//逐行读取字典
    {    
       

        //创建套接字
        sClient=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(sClient==INVALID_SOCKET)
        {
            cout<<"创建客户端socket失败!"<<endl;
            return 1;
        }
        //连接服务器
        if(connect(sClient,(sockaddr *)&addrServ,sizeof(sockaddr))==SOCKET_ERROR)
        {
            cout<<"连接服务器失败!"<<endl;
            closesocket(sClient);
            return 1;
        }
        //else
            //cout<<"连接服务器成功!"<<endl;

    char  * content = "index.php";           //请求的资源
    string str="";
    str.append("GET /");
    str.append(dir);
    str.append(" HTTP/1.1");
    str.append("\r\nHost:");
    str.append("www.beitown.com");
    str.append("\r\nAccept:*/*");
    str.append("\r\nConnection:Keep-Alive");
    str.append("\r\n\r\n");

    //发送数据
    int retval=send(sClient,str.data(),str.length()+1,0);
    if(retval==SOCKET_ERROR)
    {
        cout<<"发送数据失败!"<<endl;
    }
    //接收数据
    char buf[16];//只接受前16个字符即可
    memset(buf,0,16);
    retval=recv(sClient,buf,16,0);
    buf[15] = '\0';//字符串结尾
    if(retval==SOCKET_ERROR)
    {
        cout<<"接收数据失败!"<<endl;
    }
    cout << "/" << dir <<": >> " << buf << endl;   
    closesocket(sClient);
     
    }
    file.close();
   
    getchar();
    getchar();
    //关闭套接字,释放资源
   
    WSACleanup();
    return 0;
}

这里有几个常见问题需要注意一下:

1.Http协议通常是没有长连接这一概念的,所以在目标服务器80端口建立socket连接后,当客户端发送一次http请求,服务器响应请求之后,会主动关闭这个客户端的socket连接。
所以很多朋友会发现,当send一次数据后,再之后就会发送错误,recv也会返回-1。解决办法是,把socket的创建到关闭的整个过程都放到循环里去,即发送一次http请求之后关闭socket然后再从新建立,这个办法也解决了recv的网卡缓冲区里的数据清除,以抛弃不需要的服务器发来的网页内容。

2.
char buf[16];//只接受前16个字符即可
memset(buf,0,16);
retval=recv(sClient,buf,16,0);
buf[15] = ‘\0′;//字符串结尾
注意一定要加上最后一句 buf[15] = ‘\0′ 否则字符串无法正常结尾,会溢出并输出很多烫烫烫烫。。。

关于字典基于安全考虑这里就不提供给大家了,上一张运行截图

扫描器依次对目标网站进行了目录及文件的猜测,如图,目标网站存在index.php这个文件并且可以正常访问,返回 200 OK 代码。

以上即为一个最简的Web目录扫描器的制作过程及全部源码,更多的功能大家请自行添加,本文意在概述最简功能的实现,以此抛砖引玉,举一反三。

Hack亦有道,请勿将本篇中所阐述的内容用于非法用途。
本篇到此,谢谢关注。

BeiTown
2013.04.30

本文链接:C++网络安全工具编程(二)Web目录扫描器

转载声明:BeiTown原创,转载请注明来源:BeiTown's Coder 编码之源,谢谢


Tags: , , , ,