栏目搜索
 
 
 
 
你的位置:首页 > 程序设计相关资料 > Nessus安全测试插件编写教程 >
 

Nessus安全测试插件编写教程

发布者:[本站编辑] | 来源:[]

Nessus安全测试插件编写教程_电脑维修资料库

作者:renaud deraison(nessus最主要的编写者,法国人)
 翻译:nixe0n
 版本:1.0.0pre2

1.简介

  1.1.什么是nasl?

  nasl是一个为网络安全扫描工具nessus开发的脚本语言。通过它,任何人都可以方便快速地针对新出现的漏洞编写出测试插件,也便于不同操作系统的用户分享测试脚本。除此之外,nasl还可以保证编写的脚本只能用于针对目的主机的测试,使编写者难以使用编写的脚本用于恶意用途。


  使用nasl,你可以很容易地打造ip报文,或者发送通常的报文。nasl中还有一些专门的函数用于向ftp和web服务器发送数据。另外,nasl还可以保证:

   除了目标主机之外,不向任何的主机发送报文。
   不允许在本地系统执行任何命令。

  1.2.what nasl is not

  nasl不是一种功能很强大的脚本语言。它的目的只是用于安全测试。因此,别指望使用这种脚本语言写出第三代的web服务器或者文件转换工具,要编写此类软件还是使用perl、python或者其它的脚本语言吧。用它们编写要比使用nasl快100倍。

  另外,由于nasl的设计有些仓促,在语法上还有一些需要改进的地方。

  1.3.为什么不在nessus中使用perl、python、tcl或者其它脚本语言

  我知道有很多功能非常强大的脚本语言,和它们相比nasl功能要弱很多。不过,虽然这些语言都非常强大,但是它们都不太安全。使用这些语言,你可以非常容易地编写出木马检测插件,泄露你的信息,让第三者知道你是一个nessus用户,甚至会把一些敏感信息(例如:密码文件)发送到第三方主机。

  使用这些语言还有另外一个问题,它们都会消耗大量的系统资源,尤其是内存。这非常令人头疼。以perl为例,perl非常棒,并且非常优美。但是,如果要使用它编写nessus的测试插件,你需要消耗大量的时间安装必须的模块,net::rawip就是其中之一。

  与此相反,nasl根本不会消耗大料的内存。因此,即使没有256m内存,你也可以同时启动20个nessusd线程。而且,对于编写检测插件,nasl本身就足够了,你不必为了为了编写新的安全检测插件而安装大量的软件包。

  1.4.为什么你应该自己编写安全测试插件

  你可能会盘算为了自己编写nessus安全测试插件而在学习一种脚本语言是否值得?但是,你要知道:

   * nasl为nessus做过专门的优化,因此使用nasl编写的安全测试插件效率很高。
   * 在很多方面,nasl和c非常类似,因此你没有必要担心很难掌握。
   * nasl非常适合编写安全测试插件。
   * nasl的移植性很好。在m$版本的nessus发布之后,所有的安全测试插件根本勿需修改,就可以使用。

  1.5.这个教程会教你一些什么东西

  这个教程的目的是教你如何使用nasl编写自己的nessus安全测试插件。

  1.6.nasl的局限

  我在上面讲过,nasl不是一种强大的脚本语言。它最大的局限是:

   结构(structure)。目前nasl还不支持结构,可能在不久的将来可以支持。
   一个调试程序。nasl还没有一个合适的debug程序。不过,有一个单独的解释程序nasl可以暂时用于排错。

  1.7.感谢

  下面这些人为nasl的设计提出了高贵的意见,作者在此致谢:

   denis ducamp(denis@hsc.fr)
   fyodor(fyodor@dhp.com)
   noam rathaus(nomr@securiteam.com)

2.nasl基础:语法

  在语法上,nasl非常类似于c,只是去掉了一些烦人的东西。你勿需顾及对象的类型,也不用为它们分配和释放内存;在使用变量之前不必事先声明。这样,你就可以只致力于安全测试插件的的编写。

  如果你以前不懂c语言,读这个教程可能要费点劲,如果你对c语言已经很精通,读本教程将非常轻松。

  2.1.注释

  在nasl中,注释符是#。它只对当前行有效,例如:

  有效的注释:

   a = 1 ; #let a = 1
   #set b to 2
   b = 2;

  无效的注释:

   #
   set a to 1
   #
   a = 1;
   a = # set a to 1 # 1;

  2.2.变量、变量类型、内存分配和包含(include)

  与c语言不同,在使用变量之前,你不用事先声明,也不用关心它们的类型。如果你的操作错误(例如:把一个ip报文和一个整数相加),nasl就会提醒你。你也不必关心c语言中经常遇到的内存分配和包含(include)等问题,在nasl中没有include,而且内存是在需要时自动分配。

  2.3.数字和字符串

  nasl中的数字可以使用三种进制:十进制、十六进制和二进制。例如:

   a = 1204;
   b = 0x0a;
   c = 0b001010110110;
   d = 123 + 0xff;

  数组必须使用引号。注意:和c语言不同,除非使用string()函数,否则nasl解释器将不解释特殊字符(例如:\\n)。例如:

   a = hello\\ni'm renaud; #a等于hello\\ni'm renaud,\\n没有特殊含义
   a = string(hello\\ni'm renaud);#b等于hello
   # i'm renaud
   c = string(a); #c等于b

   string()函数将在“字符串处理”中详细讨论。

  2.4.匿名/非匿名参数

  非匿名函数(non-anonymous function)

  nasl对函数参数的处理方式也c语言也不相同。在c语言中,程序员必须只参数的位置。

  如果一个函数的参数超过10个,就非常让人头疼。例如,一个构造ip报文的函数就可能有很多参数。如果你需要使用这个函数,就得记住参数的确切次序,这非常浪费时间。

  在nasl中尽量避免出现这种情况。

  在nasl中,当函数的参数次序比较重要,并且当这个函数不同的参数是不同的类型,这个函数就是一个非匿名函数。也就是,你必须给出元素名。如果你忘记了某些元素,在运行时nasl会给你错误提示。例如:

   forge_ip_packet()函数有很多参数。以下两种调用方式都有效并且执行相同的操作:

    forge_ip_packet(ip_hl:5,ip_v:4,ip_p:ipproto_tcp);
    forge_in_packet(ip_p:ipproto_tcp,ip_v:4,ip_hl:5);

  在运行时,用户会被提示缺少参数(ip_len等)。

  匿名函数(anonymous function)

  如果函数只有一个参数,或者所有参数的类型是相同的,这种函数就叫做匿名函数。例如:

   send_packet(my_packet);
   send_packet(packet1,packet2,packet3);

  这些函数可以有选项。例如:在使用send_packet()函数时,你可以决定是否等待回应。如果你感觉没有必要接收目标的回应,你可以使用如下调用形式来加速安全测试速度:

   send_packet(packet,use_pcap:false);

  2.5.for和while

  在nasl中也存在for和while两种循环控制,和c语言的几乎完全相同,其语法格式如下:

   for(instruct_start;condition;end_loop_instruction)
    {
     #
     #需要执行的代码
     #
    }

  或者

   for(instruction_start;condition;end_loop_instruction)fuction();

  while的格式:

   while(condition)
    {
     #
     #执行的代码
     #
    }

  或者:

  while(condition)function();

  例如:

   # 显示从1到0
   for(i=1;i〈=10;i=i+1)display(i : ,i,\\n);

   # 显示从1到9以及它们是奇数还是偶数
   for(j=1;j〈=10;j=j+1)
    {
     if (j&1)display(j, is odd\\n);
      else display(j, is even\\n);
    }

   # 使用while
   i = 0;
   while(i〈10)
    {
     i=i+1;
    }

  2.6.用户定义的函数

  nasl允许用户定义自己的函数。用户可以使用如下的语法定义自己的函数:

   function my_func(argument1,argment2,....)

  用户定义的函数必须使用非匿名(non-anonymous)参数,nasl能够处理递归调用。例如:

   function fact()
    {
     if((n==0)││(n==1))
      return(n);
     else
      return(n*fact(n:n-1));
    }

   display(b! is ,fact(n:5),\\n);

  另外,用户自己定义的函数不能调用其它的用户定义函数(实际上是可以的但是遇到这种情况,nasl解释器会向你发出警告)。

  注意:如果你需要让自己的函数返回一个值,需要使用return()函数。因为return()是一个函数,因此需要有括号,下面这种写法就是错误的:

   function func()
    {
     return 1; #这种写法在c语言中是可以的,但是在nasl中不性
    }

  2.7.操作符

  一些标准的c语言操作符也可以用于nasl,包括:+、-、*、/和%。目前,nasl还不支持操作符的优先级,但是以后版本将会支持操作符的优先级。另外,nasl也支持c语言的二进制操作符│和&。

  除此之外,nasl还有两个独有的操作符:

  x操作符

  对于某些简单的循环使用for或者while非常不便,而且每次循环还需要对条件进行检查,造成效率的下降。因此nasl引入了一个x操作符来简化某些循环代码。例如:如果你需要发出10次udp报文,使用x操作符,只要下面一行代码就可以了:

   send_packet(udp)x10;

  〉〈操作符

  〉〈操作符是一个布尔型操作符,表示如果一个字符串a包含在另一个字符串b中,就返回真,例如:

   a = nessus'
   b = i like nessus;
   if(a〉〈b)
    {
     #结果为真
     display(a is contained in ,b,\\n);
    }

3.网络相关函数

  3.1.套接字处理

  套接字是使用tcp或者udp协议和其它主机通讯的途径。在nasl中不允许你直接打开一个和测试目标通讯的套接字,因此你只能使用nasl提供的函数打开套接字。

  3.1.1.如何打开一个套接字

  在nasl中,函数open_sock_tcp()和open_sock_udp()分别用于打开一个tcp或者udp套接字。这两个函数使用匿名(anonymous)参数。当前,你每次智能打开一个端口,将来的版本将解决这个问题。例如:你可以使用如下代码分别打开一个tcp和udp套接字:

   #在80端口打开一个tcp套接字
   soc1=open_sock_tcp(80);

   #在123端口打开一个udp套接字
   soc2=open_sock_udp(123);

  如果无法和远程主机建立连接,这两个函数会返回0。不过,通常open_sock_udp()不会失败,因为没有办法确定远程主机的udp端口是否开放,对于open_sock_tcp(),如果远程主机的端口是关闭的,它就会返回0。

  open_sock_tcp()可以用于对tcp端口的简单扫描,例如:

   start = prompt(first port to scan?); #输入开始的端口
   end = prompt(last port to scan?); #输入结束的端口
   for(i=start;i〈end;i=i+1)
    {
     soc=open_sock_tcp(i);
     if(soc)
      {
       display(port ,i, is open\\n);
       close(soc);
      }
    }

  3.1.2.关闭一个端口

  关闭一个端口使用close()函数,在close()内部,关闭端口之前,它首先会调用shutdown()函数。

  3.1.3.读写套接字

  根据被读写的套接字类型,可以选择使用如下函数完成这两项操作:

   recn(socket:〈socketname〉,length:〈length〉 <,timeout:〈timeout〉>)

  从套接字〈socketname〉读取〈length〉个字节,这个函数可以用于tcp和udp。超时(timeout)参数是可选的,以秒为单位。

   recv_line(socket:〈socketname〉,length:〈length〉 <,timeout:〈timeout〉>)

  这个函数和recv()函数类似,只是如果遇到换行(\\n)操作终止。这个函数只能用于tcp套接字。

   send(socket:〈socket〉,data:〈data〉 <,length:〈length〉>)

  从套接字〈socket〉发送数据〈data〉。可选参数length告诉函数发送〈length〉字节。如果没有设置length,发送操作就在遇到null时终止。

  如果没有设置超时参数,读函数(recv()和recv_line())就使用默认的超时时间5秒。如果时间到,它们就返回false。例如:

   # 以下代码用于显示远程主机的banner信息
   soc = open_sock_tcp(21);
   if(soc)
    {
     data = recv_line(socket:soc,length:1024);
     if(data)
      {
       display(the remote ftp banner is : \\n,data,\\n);
      }
     else
      {
       display(the remote ftp server seems to be tcp-wrapper\\n);
      }
     close(soc);
    }

  3.1.4.高层操作

  nasl有一些针对ftp和www协议的函数,用于简化对这两个应用层协议的某些操作。

   ftp_log_in(socket:〈soc〉,user:〈login〉,pass:〈pass〉)

  尝试通过〈soc〉套接字登录到远程fp主机。如果用户名〈login〉和密码〈pass〉都正确,就返回true,否则返回false。

  ftp_get_pasv_port(socket:〈soc〉)

  向远程ftp服务器发出一个pasv命令,获得连接的端口。nasl脚本可以通过这个端口从ftp服务器下载数据。如果发生错误函数将返回false。

  is_cgi_installed(〈name〉)

  测试远程web服务器是否安装了名为〈name〉的cgi程序。这个函数向远程web服务器发出get请求实现这个目的。如果〈name〉不是以斜杠(/)开头,就认为它是相对于/cgi-bin/。

  这个函数也可以用于确定某个文件是否存在。

  示例脚本:

   #
   # 针对www服务器的测试
   #
   if(is_cgi_installed(/robots.txt))
    {
     display(the file /robots.txt is present\\n);
    }

   if(is_cgi_installed(php.cgi))
    {
     display(the cgi php.cgi is installed in /cgi-bin/\\n);
    }

   if(!is_cgi_installed(/php.cgi))
    {
     display(there is no php.cgi in the remote web root\\n);
    }

   #
   # 针对ftp服务器的测试
   #
   # 打开一个连接
   soc = open_sock_tcp(21);
   # 匿名登录到远程ftp主机
   if(ftp_log_in(socket:soc,user:anonymous,pass:joe@))
    {
     # 打开一个被动传输模式的端口
     port = ftp_get_pasv_port(socket:soc);
     if(port)
      {
       soc2 = open_sock_tcp(port);
       #尝试获得远程系统的/etc/passwd文件
       data = string(retr /etc/passwd\\r\\n);
       send(socket:soc,data:data);
       password_file = recv(socket:soc2,length:10000);
       display(password_file);
       close(soc2);
      }
     close(soc);
    }

  3.2.原始报文处理

  nasl允许用户构造自己的ip报文,而且报文的定制是以一种智能的方式进行的。例如,如果你改变了一个tcp报文的某个参数,就会造成其tcp校验和发生改变,但是你不必为此费心,nasl会自动完成。

  所有的原始报文构造函数都使用非匿名(non-anonymous)参数。参数的名字都是来自bsd的包含文件。因此一个ip报文的长度域叫做ip_len而不是length。

  3.2.1.构造ip报文

  在nasl中,你可以使用forge_ip_packet()函数构造一个新的ip报文;使用set_ip_element()函数获得报文某个域的值;使用set_ip_element()函数改变现有ip跋文某个域的值。forge_ip_packet函数的原形如下:

   〈return_value〉=forge_ip_packet(
     ip_hl :〈ip_hl〉,
     ip_v :〈ip_v〉,
     ip_tos :〈ip_tos〉,
     ip_len :〈ip_len〉,
     ip_id :〈ip_id〉,
     ip_off :〈ip_off〉,
     ip_ttl :〈ip_ttl〉,
     ip_p :〈ip_p〉,
     ip_src :〈ip_src〉,
     ip_dst :〈ip_dst〉,
     );

  其中,ip_sum参数是可选的,如果没有使用,nasl会自动计算报文的校验和。ip_p参数可以是一个整数值,或者是ipproto_tcp、ipproto_udp、ipproto_icmp、ipproto_igmp或者ipproto_ip等常量中的某个值。

  get_ip_element()函数的原型如下:

   〈element〉=get_ip_element(
     ip :〈ip_varible〉,
     element :ip_hl│ip_v│ip_tos│ip_len│
     ip_id│ip_off│ip_ttl│ip_p│
     ip_sum│ip_src│ip_dst);

  get_ip_element()将返回报文中的某个域的值。element参数必须是ip_hl、ip_v、ip_tos、ip_len、ip_id、ip_off、ip_ttl、ip_p、ip_sum、ip_src、ip_dst中的一个,而且引号是必不可少的。

  set_ip_elements()函数的原型如下:
   set_ip_elements(
    ip :〈ip_variable〉,
    
    
    
    
    
    
    
    
    
    
    
   );

  这个函数可以改变ip报文〈ip_varible〉的值,如果你没有修改ip_sum域的值,它会自动重新计算。这个函数没有构造报文的能力,因此需要把它放在forge_ip_packet()函数之后。

  最后,还有一个函数dump_ip_packet()需要说明一下,这个函数能够以可读的方式把ip报文的内容输出到屏幕。这个函数应该只用于调试目的。

  3.2.2.构造一个tcp报文

  forge_tcp_packet()用来构造tcp报文。函数原型如下:

   tcppacket = forge_tcp_packet(
    ip :〈ip_packet〉,
    th_sport :〈source_port〉,
    th_dport :〈destination_port〉,
    th_flags :〈tcp_flags〉,
    th_seq :〈sequence_number〉,
    ,
    th_off :〈offset〉,
    th_win :〈window〉,
    th_urp :〈urgent_pointer〉,
    th_sum :〈checksum〉,
    );

  其中,标志参数th_flags必须是th_syn、th_ack、th_fin、th_push或者th_rst,这些标志可以使用│操作符结合到一块。th_flags还可以使用一个整数值。ip_packet必须首先由forge_ip_packet()函数产生或者使用send_packet()、pcap_next()函数得到的返回值。

  函数set_tcp_elements()能够修改tcp报文的内容,其原型如下:

   set_tcp_elements(
    tcp :〈tcp_packet〉,
    
    
    
    
    
    
    
    
    
    
    );

  除非你自己设置th_sum参数,否则函数会自动计算报文的校验和。

  函数get_tcp_element()用来设置tcp报文的内容,其原型如下:

   element = get_tcp_elements(
    tcp :〈tcp_packet〉,
    element :〈element_name〉);

  element_name必须是tcp_sport、th_dport、th_flags、th_seq、th_ack、th_x2、th_off、th_win、th_urp、th_sum其中之一。注意:引号是不可缺少的。

  3.2.3.构造udp报文

  udp报文构造函数forge_udp_packet()和tcp构造函数极为类似,其原型如下:

   udp = forge_udp_packet(
    ip :〈ip_packet〉,
    uh_sport :〈source_port〉,
    uh_dport :〈destination_port〉,
    uh_ulen :〈length〉,
    
    );

  而set_udp_elements()和get_udp_elements()函数和tcp报文对应的处理函数用法也相同。

  3.2.4.构造icmp报文
  3.2.5.构造igmp报文
  3.2.6.发送报文

  构造报文的操作完成之后,你可以使用send_packet()函数将它们发送出去,其原型如下:

   reply = send_packet(packet1,packet2,....,packetn,
    pcap_active:〈true│false〉,
    pcap_filter:〈pcap_filter〉);

  如果pcap_active参数为true,这个函数就会等待目标的回应。pcap_filter用来设置你需要得到的报文类型,详情请参考pcap或者tcpdump的手册页。

  3.2.7.读取报文

  你可以使用pcap_next()函数读取一个报文,其原型如下:

   reply = pcap_next();

  这个函数将从你使用的最后一个接口读取一个报文,报文的类型取决于最后设置的pcap类型。

  3.3.工具函数

  nasl还提供了一些工具函数以简化你的编程。

   this_host()

  获得运行脚本的主机ip地址,没有参数。

   get_host_name()

  返回当前被测试主机的主机名,没有参数。

   get_host_ip()

  返回当前被测试主机的ip地址,没有参数。

   get_host_open_port()

  获得远程主机打开的第一个端口号,没有参数。这个函数对于某些脚本(例如:land)非常有用,有些tcp序列号分析程序需要通过这个函数获得远程主机一个打开的端口。

   get_port_stat(〈portnum〉)

  如果tcp端口〈portnum〉打开或者其状态是未知,就返回true。

   telnet_init(〈soc〉)

  在一个打开的套接字上初始化一个telnet会话,并且返回telnet数据的第一行。例如:

   soc = open_sock_tcp(23);
   buffer = telnet_init(soc);
   display(the remote telnet banner is ,buffer,\\n);
   tcp_ping()

  如果远程主机应答tcp ping请求(发送一个设置ack标志的tcp报文),本函数就返回true,没有参数。

   getrpcport()

  获得远程主机的rpc端口号,原型为:

   result = getrpcport(program :〈program_number),
    protocol :〈ipproto_tcp│ipproto_udp,
    );

  如果远程主机的〈program_number〉程序没有在rpc portmap监控进程中注册就返回0。

4.字符串处理函数

  nasl允许你象处理数字一样处理字符串。因此,你能够安全地使用==、〈和〉等操作符。

  例如:
   
   a = version 1.2.3;
   b = version 1.4.1;
   if(a〈b)
    {
     #因为version 1.2.3比version 1.4.1低
     #因此,开始执行这里的代码
    }
   c = version 1.2.3;
   if(a == c)
    {
     #两个字符串相等
     #因此执行这里的代码
    }

  在nasl中,也可以获得一个字符串的某个字符,和c语言完全相同,例如:

   a = test;
   b = a<1>; #b等于e

  你也可以在一个字符串中加、减一个字符串,例如:

   a = version 1.2.3;
   b = a - version; #b等于1.2.3
   a = this is a test;
   b = is a ;
   c = a - b; #c等于this test
   a = test;
   c = is a ;
   c = a - b; #a等于testtest

  除此之外,〉〈也可以用于字符串的处理。nasl有很多函数来构造或者修改字符串:

  4.1.处理正则表达式的ereg()函数

  在nasl中,模式匹配是由ereg()函数完成的。原型如下:

   result = ereg(pattern:〈pattern,string:〈string〉)

  正则表达式的语法是egrep风格的。细节请参考egrep的手册页。例如:

   if(ereg(pattern:.*,string:test))
    {
     display(always execute\\n);
    }

   mystring=recv(socket:soc,length:1024);
   if(ereg(pattern:ssh-.*-1\\..*,string:mysting))
    {
     display(ssh 1.x is running on this host);
    }

  4.2.egrep()函数

  egrep()函数返回一个多行文本中,匹配〈pattern〉的第一行。如果对单行文本使用这个函数,它就相当于ereg()。如果没有匹配的行,它就返回false。其原型如下:

   str=egrep(pattern:〈pattern〉,string:〈string〉)

  示例:

   soc=open_soc_tcp(80);
   str=string(head / http/1.0\\r\\n\\r\\n);
   sen(socket:soc,data:str);
   r=recv(socket:soc,length:1024);
   server=egrep(pattern:^server.*,string:r);
   if(server)display(server);

  4.3.crap()函数

  crap()函数非常便于测试缓冲区溢出,有两种原型:

   crap(〈length〉)

  获得一个以'x'填充的长度为〈length〉的字符串。

   crap(length:〈length〉,data:〈data〉)

  高一个长度为〈length〉的字符串,并使用〈data〉填充字符串。例如:

   a=crap(5); #a=xxxxx
   b=crap(4096); #b=xxx...xxx(4096个x)
   c=crap(length:12,data:hello); #c=hellohellohe

  4.4.string()函数

  这个函数用来定制字符串。其原型为:

   string(〈string1〉,<〈string2〉,...,〈stringn〉>);

  它能够解释字符串中\\n、\\t等特殊字符。例如:

   name=renand;
   a=string(hello,i am ,name,\\n_; #a等于hello,i am renaud
   #末尾回行
   b=string(1, and m,2, make ,1+2); #b等于1 and 2 make 3
   c=string(mkd ,crap(4096),\\r\\n); #c等于mkd xxx...xxxxx(4096个x)
   #后面是一个回车和一个回行

  4.5.strlen()函数

  strlen()函数返回一个字符串的长度,例如:

  a==strlen(abcd); #a等于4

  4.6.raw_string()函数

  这个函数能够把数字转换为对应的字符,例如:

   a=raw_string(80,81,82); #80、81、82分别对应ascii字符的pqr

  4.7.strtoint()函数

  这个函数把一个nasl整数转换为一个二进制整数。原型为:

   value=strtolen(number:〈nasl_integer〉,size:〈number_of_byte〉);

  这个函数比较适合于和raw_string()函数一块使用。size参数是nasl整数的字节数,可以是:1、2、4。

  4.8.tolower()函数

  这个函数能够把一个字符串中的所有大写字符转换为小写字符。原型为:

   string2=tolower(〈string〉);

  例如:

   a=hello,world
   b=tolower(a); #b等于hello,world

5.总结

  这是nasl reference guide的第一部分,主要介绍了nasl的各种函数。在下一部分我们将系统地介绍如何编写nessus安全测试插件。'
1.怎样编写一个高效的nessus安全测试插件

  在nessus安全测试系统中,所有的安全测试都是由nessusd进程发动的。在测试期间,一个好的测试插件必须能够有效地利用其它测试插件的测试结果。例如:一个测试插件需要打开一个到ftp服务器的连接,而在这之前它应该首先检查端口扫描测试插件的结果,确定ftp端口是否打开。在一般情况下,这样只会节约一点点时间,但是如果被测试主机位于防火墙之后,这样做会节省由于防火墙丢弃到21端口的tcp报文造成的漫长等待时间。


  1.1.确定端口是否打开

  get_port_state(〈portnum〉)函数用于获得端口的状态。如果端口为开,这个函数就返回true;反之,则返回false;如果这个端口没有被扫描过,也就是其状态为未知(unknown),函数也将返回true。这个函数只消耗很少的cpu资源,因此你可以尽可能地使用它,来提高测试插件的效率。

  1.2.基础信息(knowledge base,kb)

  在测试过程中,nessus会为每个主机维护一份由扫描测试插件获得的基本信息(knowledge base,这个词本来应该是基础知识的意思,但是这里似乎翻译作基本信息更为恰当^_^)。各种其它的测试插件应该尽可能地利用这些信息,以提高测试效率。实际上,端口的状态就保存在这里。

  kb被分为好几类。service类包含每个已知的服务和为其分配的端口号。例如,在大多数情况下,server/smtp的值为25。但是,如果远程主机的smtp服务被隐藏于2500端口,这个值就改为2500。

  有关基本信息各个元素的细节请参考附录b。

  在nasl中,有两个有关节本信息(kb)的函数。使用get_kb_item(〈name〉)函数可以获得基本信息的〈name〉项的值,这个函数是匿名函数;而函数set_kb_item(name:〈name〉,value:〈value〉)能够把〈name〉项的值设置为〈value〉。

  注意:你不能获得刚刚加入的基本信息条目的值。例如,以下代码将无法象你所期待的那样执行:

   set_kb_item(name:attack,value:true);
   if(get_kb_item(attack))
    {
     #这里的代码不可能执行
     #因为attack基本信息项并没有更新
    }

  之所以会这样,是出于安全和代码稳定性的考虑。在安全测试期间,nessus服务器会为每个安全测试插件维护一份基本信息(kb)拷贝,安全测试插件只是从自己的基本信息(kb)拷贝中获得信息。而set_kb_item()函数只更新原始的基本信息(kb)拷贝,不对当前安全测试插件使用的拷贝进行更新操作。

2.nasl脚本结构

  每个安全测试插件需要向nessus服务器进行注册后,才能使用。注册信息包括名字、描述、作者等。每个nasl脚本都需要有以下结构:

   #
   #nasl基本基本结构
   #
   if(description)
    {
     #这里是注册信息
     #
     #这里可以叫做注册部分(register section)
     #
     exit(0);
    }
   #
   #这里是脚本代码。我们可以称为攻击部分(attack section)
   #

  description是一个全局变量,值可以是true或者false,取决于脚本是否需要注册。

  2.1.注册部分

  在脚本的注册部分,必须调用以下函数:

   script_name(language1:〈name〉,<...>)

  设置在nessus客户程序窗口中显示的名称。

   script_description(language1:〈desc〉,<...>)

  设置在nessus客户程序中显示的描述信息。

   script_summary(language1:〈summary〉,<...>)

  设置总结信息,必须在一行之内总结描述信息的内容。

   script_category(〈category〉)

  设置脚本的类别。必须是act_attack、act_gather_info、act_denial和act_scanner之一。

   act_gather_info

  信息采集类脚本。这种脚本率先启动,不会对远程主机造成伤害。

   act_attack

  这类脚本会尝试获得远程主机的某些权限,可能会危害远程主机(例如,如果运行缓冲区溢出测试插件)

   act_denial

  这种脚本会发起拒绝服务攻击,试图造成远程主机宕机。

   act_scanner

  端口扫描脚本。

   script_copyright(language1:〈copyright〉,<...>)

  设置脚本的版权信息。

   script_family(language1:〈family〉,<...>)

  设置脚本所属的族(family)。nasl对此没有明确的规定,你可以任意定义脚本所属的族,例如:nixe0n's powertools,不过我不建议这样做。当前使用的族名有:

   backdoors
   cgi abuses
   denial of service
   ftp
   finger abuses
   firewalls
   gain a shell remotely
   gain root remotely
   misc
   nis
   rpc
   remote file access
   smtp problems
   useless services

  你可能注意到了,以上所有的函数都有一个叫做language1的参数。这个参数用于提供多语言支持。使用nasl编写的脚本都需要支持英语,因此这些函数的确切语法是:

   script_fuction(english:english_text,);

  除了以上函数,还有一个用于解决安全测试插件依赖关系的函数script)dependencies()。它告诉nessusd服务器在某些脚本之后启动当前脚本。如果当前脚本需要其它脚本获得的结果,就需要使用这个函数。其原型为:

   script_dependencies(filename1 <,filename2,...,filenamen>);

  filename参数是脚本文件名。

  2.2.攻击部分

  脚本的攻击部分可以包括所有用于攻击测试的代码。一旦攻击完成,你可以使用security_warning()和security_hole()函数报告是否存在此类安全问题。这两个函数的用途基本相同,security_warning()用于攻击成功,但是问题不大的情况。它们的原型如下:

   security_warning(〈port〉 <,protocol:〈proto〉>);
   security_hole(〈port〉 <,protocol:〈proto〉>);
   security_warning(port:〈port〉,data:〈data〉 <,protocol:〈proto〉>);
   security_hole(port:〈port〉,data:〈data〉 <,protocol:〈proto〉>);

  在上面的第一种情况下,客户程序显示的内容是脚本注册时script_description()函数提供的。由于能够支持多语言,因此非常方便。

  在第二种情况下,客户程序将显示data参数的内容。如果你需要显示动态获得的数据,就必须使用这种形式。

  2.3.cve兼容性

  cve是麻省理工学院维护的一个数据库,主要是对安全相关的问题提供一个一般的描述。

  详情请参考http://cve.mitre.org。

  nessus和cve完全兼容,如果你要测试一个cve定义过的安全问题,就可以在插件脚本的描述部分调用script_cve_id()函数。其原型如下:

   script_cve_id(string);

  例如:

   script_cve_id(cve-1999-0991);

  如果使用了这个函数,nessus客户程序在生成报告时,会自动引用相关的cve记录。

  2.4.示例

  除了安全测试外,nasl也可以用来编写一些用于维护的脚本。下面就是一个例子,用户可以使用这个脚本检查那些主机正在提供ssh服务。
   #
   #检查ssh
   #
   if(description)
    {
     script_name(english:ensure the presence of ssh);
     script_description(english:this script makes sure that ssh is running);
     script_summary(english:connects ont remote tcp port 22);
     script_category(act_gather_info);
     script_family(english:admiminstration toolbox);
     script_copyright(english:this script was writtern by joe u.);
     exit(0);
    }
   #
   #ssh服务可能隐藏在别的端口
   #因此我们需要依赖于find_service插件获得的结果
   #
   port=get_kb_item(services/ssh);
   if(!port)port=22;
   #首先声明ssh没有安装
   ok=0;
   if(get_port_state(port))
    {
     soc=open_sock_tcp(port);
     if(soc)
      {
       #检查端口是否是由tcp_wrapper封装的。
       data=recv(socket:soc,length:200);
       if(ssh〉〈data)ok=1;
      }
     close(soc);
    }
   #
   #报告不提供ssh服务的主机
   #
   if(!ok)
   {
    report=ssh is not running on this host!;
    security_warning(port:22,data:report);
   }

3.脚本优化

  在安全测试期间,nessusd服务器将启动200多个脚本。如果所有脚本编写的都不好,这个测试就会浪费大量的时间。因此,你必须尽量提高脚本的效率。

  3.1.只在必要时运行

  对于优化脚本,最有效的方法是告诉nessusd服务器什么时候不要启动它。例如,假设你的脚本需要建立到远程主机123/tcp端口的连接,如果nessusd知道这个端口已经被关闭,就没有必要启动你的脚本了。script_require_ports()、script_require_keys()和script_exclude_keys()就是用来实现上述目的。这些脚本需要在描述部分调用:

   script_require_ports(〈port1〉,〈port2〉,...)

  参数中的至少一个端口开放才启动脚本。参数可以是数字,也可以是基本信息(kb)中定义的符号,例如:services/www。注意:如果端口的状态是未知的(例如:还没有进行过端口扫描),这个脚本也会执行。

   script_require_keys(〈key1〉,〈key2〉,...)

  只有参数中的关键词在基本信息(kb)都有定义时,才执行脚本。例如:

   script_require_keys(ftp/anonymous,ftp/writeable_dir);

  表示只有远程ftp主机支持匿名用户以及存在可以写的目录时,才启动当前脚本。

   script_exclude_keys(〈key1〉,〈key2〉,...)

  参数表示的关键词至少有一个在基本信息(kb)中有定义,才执行当前脚本。

  3.2.充分利用其它脚本的结果

  充分利用基本信息拷贝中的信息,可以使脚本更高效。例如,如果在调用open_sock_tcp()函数之前,先调用get_port_state()函数,就可以避免由于目标端口是关闭的带来的时间上的浪费。

4.如何分享你的新脚本

  如果你想让别人分享自己的成果,在编写测试脚本时要遵循以下原则:

   你的脚本不能存在任何与用户交互的操作
   nasl安全测试脚本是在服务器端运行的,因此用户看不到任何的输出信息。
   一个脚本只能测试一个漏洞
   如果你知道如何测试好几个漏洞,那就为每个漏洞都编写自己的测试脚本。
   你的脚本最好归入现有的种类
   如果你计划分享自己的成果,最好避免建立joe's power tools这样的新插件种类,尽量把插件划入已有的插件种类。
   查询cve中是否有相关漏洞的定义
   如果你能够注意脚本的兼容性,可以节省nessus维护者的很多时间。
   把成果发给nessus维护者

   nessus的维护者就是本文的作者。如果你不象独享自己的成果,就把它发给nessus维护者。如果你的脚本被采用,它就会被分配一个唯一的id。

5.结论

  希望你能够喜欢这个教程。学习这个语言不会占用你太多的时间,你需要多多练习。在使用过程中,你会发现nasl解释器的一些bugs,希望你能够及时把这些bugs报告给我。

附录a.基本信息(knowledge base)

  所谓的基本信息是一些关键词,里面包含其它测试插件获得的信息。使用script_dependencies()、get_kb_item()和set_kb_item()函数,可以帮助你避免没有必要的重复测试。附录a中将罗列出这些关键词。

  在基本信息(kb)中,每个项可以有几个值。例如,远程主机运行两个ftp服务:一个在端口21,另一个在端口2100。这样关键词services/ftp就等于21和2100两个端口。在这种情况下,测试脚本将执行两次:第一次,get_kb_item(services/ftp)函数将返回21,第二次这个函数将返回2100。不过,这是自动进行的,无须人工干预。对于脚本编写者来说,相当于每个基本信息关键词只有一个值。有些关键词目前没有多大用处,好多我就没有用过,但是有备无患。

  host/os
  定义文件:queso.nasl、nmap_wrapper.nasl
  类型:字符串
  含义:远程操作系统的类型

  host/dead
  定义文件:ping_host.nasl和所有的dos插件
  类型:boolean
  含义:远程主机关闭。如果这个项被设置,nessusd将终止针对这个主机的所有测试。

  services/www
  定义文件:find_service.nes
  类型:端口号
  含义:目标主机web服务器监听的端口号。如果没有发现web服务器,就返回0。

  services/auth
  定义文件:find_service.nes
  类型:端口号
  含义:identd服务使用的端口。如果没有这个服务,就返回0。

  services/echo
  定义文件:find_service.nes
  类型:端口号
  含义:echo服务使用的端口。如果没有这个服务,就返回0。

  services/finger
  定义文件:find_service.nes
  类型:端口号
  含义:finger服务使用的端口。如果没有这个服务,就返回0。

  services/ftp
  定义文件:find_service.nes
  类型:端口号
  含义:ftp服务使用的端口。如果没有这个服务,就返回0。

  services/smtp
  定义文件:find_service.nes
  类型:端口号
  含义:smtp服务使用的端口。如果没有这个服务,就返回0。

  services/ssh
  定义文件:find_service.nes
  类型:端口号
  含义:ssh服务使用的端口。如果没有这个服务,就返回0。

  services/http_proxy
  定义文件:find_service.nes
  类型:端口号
  含义:http代理服务使用的端口。如果没有这个服务,就返回0。

  services/imap
  定义文件:find_service.nes
  类型:端口号
  含义:imap服务使用的端口。如果没有这个服务,就返回0。

  services/pop1
  定义文件:find_service.nes
  类型:端口号
  含义:pop1服务使用的端口。如果没有这个服务,就返回0。

  services/pop2
  定义文件:find_service.nes
  类型:端口号
  含义:pop2服务使用的端口。如果没有这个服务,就返回0。

  services/pop3
  定义文件:find_service.nes
  类型:端口号
  含义:pop3服务使用的端口。如果没有这个服务,就返回0。

  services/nntp
  定义文件:find_service.nes
  类型:端口号
  含义:nntp服务使用的端口。如果没有这个服务,就返回0。

  services/linuxconf
  定义文件:find_service.nes
  类型:端口号
  含义:linuxconf服务使用的端口。如果没有这个服务,就返回0。

  services/swat
  定义文件:find_service.nes
  类型:端口号
  含义:swat服务使用的端口。如果没有这个服务,就返回0。

  services/wild_shell
  定义文件:find_service.nes
  类型:端口号
  含义:shell服务使用的端口。如果没有这个服务,就返回0。

  services/telnet
  定义文件:find_service.nes
  类型:端口号
  含义:telnet服务使用的端口。如果没有这个服务,就返回0。

  services/server
  定义文件:find_service.nes
  类型:端口号
  含义:realserver服务使用的端口。如果没有这个服务,就返回0。

  services/netbus
  定义文件:find_service.nes
  类型:端口号
  含义:netbus服务使用的端口。如果没有这个服务,就返回0。

  bind/version
  定义文件:bind_version.nasl
  类型:字符串
  含义:远程bind监控程序的版本

  rpc/bootparamd
  定义文件:bootparamd.nasl
  类型:字符串
  含义:bootparam rpc服务正在运行

  windows compatible
  定义文件:ca_unicenter_file_transfer_service.nasl、ca_unicenter_transport_service.nasl、mssqlserver_detect.nasl和windows_detect.nasl
  类型:boolean
  含义:远程主机好象运行一种windows兼容操作系统(只有相关的端口开放才进行这个测试)

  finger/search.**@host
  定义文件:cfinger_search.nasl
  类型:boolean
  含义:使用**进行finger查询能够得到用户列表。

  finger/0@host
  定义文件:finger_0.nasl
  类型:boolean
  含义:使用0进行finger查询能够得到用户列表

  finger/.@host
  定义文件:finger_dot.nasl
  类型:boolean
  含义:使用.进行finger查询能够获得用户列表

  finger/user@host1@host2
  定义文件:finger_0.nasl
  类型:boolean
  含义:finger监控程序能够用于重定向攻击

  www/frontpage
  定义文件:frontpage.nasl
  类型:boolean
  含义:远程web服务器使用frontpage扩展

  ftp/anonymous
  定义文件:ftp_anonymous.nasl
  类型:boolean
  含义:远程ftp服务器可以匿名登录

  ftp/root_via_cmd
  定义文件:ftp_cwd_root.nasl
  类型:boolean
  含义:使用cwd命令可以获得远程ftp服务器的root权限。参看cve-1999-0082

  ftp/microsoft
  定义文件:ftp_overflow.nasl
  类型:boolean
  含义:远程主机是micro$oft ftp服务器,不能处理太长的参数

  ftp/false_ftp
  定义文件:ftp_overflow.nasl
  类型:boolean
  含义:远程主机经过tcp封装,或者ftp端口开放而连接关闭。

附录b.nasl工具

  libnasl软件包中有一个单独的解释器nasl,可以用于脚本的调试。更多细节可以参考man nasl。</t