查看: 1394|回复: 1

如何通过Sqlmap实现子进程控制

[复制链接]
  • TA的每日心情
    慵懒
    2016-5-8 01:28
  • 签到天数: 36 天

    连续签到: 1 天

    [LV.5]常住居民I

    发表于 2015-12-15 13:49:16 | 显示全部楼层 |阅读模式
    0×00 场景

    SQLMap是检测SQL注入漏洞公认的神器,其本身并不支持作为模块导入使用,但是提供了sqlmapapi.py ,它能够启动一个基于bottle的API服务器,对外提供了丰富的API接口。在我们的一些内部应用中,有用到sqlmapapi来调用sqlmap进行大规模探测。

    我们启用了多个Celery的worker,每个worker中使用gevent协程,向一个sqlmapapi server中下任务,在长时间执行后,在日志中出现大量OSError: Too many open files的报错。解决该问题后,又出现了调用sqlmapapi中的stop函数来关闭超时扫描任务时,任务均变为僵尸进程的问题。

    作为忠实的SQLMap粉丝,我们向官方提交了一些issue,但不知何故,在收到官方的一次反馈后就没有后续了。只好自己动手,丰衣足食了。

    0×01 解决 Too many open files

    -        堆栈信息

    Traceback (most recent call last):
    File "/opt/sqlmap/thirdparty/bottle/bottle.py", line 763, in handle
    return route.call(*args)
    File "/opt/sqlmap/thirdparty/bottle/bottle.py", line 1627, in wrapper
    rv = callback(a, *ka)
    File "/opt/sqlmap/thirdparty/bottle/bottle.py", line 1577, in wrapper
    rv = callback(a, **ka)
    File "/opt/sqlmap/lib/utils/api.py", line 460, in scan_start
    DataStore.tasks[taskid].engine_start()
    File "/opt/sqlmap/lib/utils/api.py", line 159, in engine_start
    shell=False, stdin=PIPE, close_fds=not IS_WIN)
    File "/usr/lib/python2.7/subprocess.py", line 672, in __init_
    errread, errwrite) = self._get_handles(stdin, stdout, stderr)
    File "/usr/lib/python2.7/subprocess.py", line 1038, in _get_handles
    p2cread, p2cwrite = self.pipe_cloexec()
    File "/usr/lib/python2.7/subprocess.py", line 1091, in pipe_cloexec
    r, w = os.pipe()
    OSError: [Errno 24] Too many open files
    -        Sqlmapapi相关源码

    def engine_start(self):
       self.process = Popen(["python", "sqlmap.py", "--pickled-options", base64pickle(self.options)], shell=False, stdin=PIPE, close_fds=not IS_WIN)
    -        分析

    根据上面的代码和报错,可以看到问题出现在新建PIPE这里,考虑是由于大量任务开启的无用PIPE句柄过多,而这里的调用并不需要对输入做处理,源码中将stdin定向到PIPE是多余的。 去掉 stdin=PIPE 后,不再出现这个错误,问题成功解决。

    -subprocess.PIPE 和 close_fds
    我们第一次提交这个issue, 官方给的解决办法是,在suprocess.Popen()中加入一个close_fds=True的参数,这也是一个Python网络编程中常见的一个小坑,但在这里并没有解决我们的问题。至于为什么,让我们从close_fds来说起。这个参数的含义是,在子进程执行之前,关闭所有除0, 1, 2之外所有的文件描述符。

    我们知道,子进程会继承父进程几乎所有的资源,这里面包括父进程打开的文件描述符。在网络编程中,如果你没有意识到,子进程也打开了一个父进程的socket文件,那么当你想要close()连接的时候,很可能会出现让你摸不着头脑的错误。

    (注: 这篇文章里列出了一些Python中常见的坑,值得阅读。)

    在我们这个场景里,SQLMap的作者以为是子进程继承了多余的PIPE文件,所以造成了这个错误,这的确也是一个应当注意的点,但是我们的任务下的太多,而创建的PIPE没有关闭,光主进程里打开的文件句柄也超过了系统限制。

    切记,Popen并不会为你关闭PIPE,需要你主动调用PIPE.close()或者使用subprocess.communicate来替你关闭它。

    0×02 解决僵尸进程

    -        僵尸进程

    内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、已经该进程使用的CPU时间总量。在UNIX术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵尸进程(zoombie)。

    – 《UNIX环境高级编程(第二版)》

    -        Sqlmapapi相关源码

    def engine_stop(self):
         if self.process:
             return self.process.terminate()
         else:
             return None
    -        分析

    当调用terminate子进程结束后,父进程并没有去调用wait()或者waitpid()来接收SIGCHLD信号,导致子进程未正常结束。在terminate()后增加wait()函数来回收子进程的资源,这样就不会再出现僵尸进程了。

    def engine_stop(self):
        if self.process:
            self.process.terminate()
            return self.process.wait()
        else:
            return None
    -        wait与waitpid

    对于subprocess模块,我们只需要简单地调用Popen.wait()这个函数,就可以很方便地回收子进程的资源了。如果需要更高级的操作,需要使用os模块中的wait*系列函数。这里简单介绍一下waitpid的用法。

    Wait()这个函数是阻塞的,如果父进程有多个子进程,wait()会阻塞到第一个子进程的结束。Waitpid()则可以指定等待某个特定的子进程的结束,而且它还支持一个WNOHANG的选项,来让该函数立即返回,不阻塞。

    如果一个父进程有多个子进程,而我们只调用一次wait(),是不足以防止出现僵尸进程的。这是我们需要waitpid()的原因。

    while( (pid = waitpid(-1, &stat,WNOHANG)) > 0) # os.waitpid中不需要第二个参数
    printf(“child %d terminated\n” , pid);
    子进程在父进程之前终止,父进程应该调用上面两个函数之一去获取子进程终止状态。那么如果父进程比子进程先终止呢?那么,对于父进程已经终止的所有进程,他们的父进程都变为init进程。而一个由init进程领养的进程终止是不会变为僵尸进程的,因为init被编写为无论何时只要有一个子进程终止,init就会调用一个wait函数取得其最终状态。基于此,《UNIX环境高级编程》中也给出了一个通过fork两次来避免僵尸进程的方法,具体见书中程序清单8-5。

    发文前,解决这两个问题的pull request也被sqlmap官方repo merge.

    0×03 如何优雅地处理子进程

    像SQLMap这样优秀而成熟的开源应用也会在进程处理这块百密一疏,因此我们把进程调用的场景做了一些总结,也提供了代码片段以供参考。

    1,如果对子进程的输入输出感兴趣,可以调用communicate()来获取;如果对子进程的输入输出不感兴趣,且希望等待这个进程的结果,可以使用call(),这两个函数都会wait()回收子进程。
    2,对于可能运行很长时间的子进程,我们可以设置一个timeout值,在这个值的时间范围内,轮询地去取输出(如果有输出的话),也可以调用subprocess.poll()函数去查看进程是否结束。当超过timeout后,可以直接调用kill()去清理这个进程。
    使用poll()的方法可以参考sqlmapapi的源码, 我们在这里也提供一段比较完整的代码片段来优雅地处理子进程,使之不会出现僵尸或者游离的子进程。

    def run_wait(process, timeout, _sleep_time=.1):
            for _ in xrange(int(timeout * 1. / _sleep_time + .5)):
                time.sleep(_sleep_time)
                out = process.stdout.readline()
                if out == "":
                    return process.wait()
                else:
                    sys.stdout.write(out)
                    sys.stdout.flush()
            raise VulScanTimeoutException
        def kill_child_processes(parent_pid, sig=signal.SIGTERM):
            try:
                p = psutil.Process(parent_pid)
            except psutil.error.NoSuchProcess:
                return
            child_pid = p.children(recursive=True)
            for pid in child_pid:
                os.kill(pid.pid, sig)

        try:
            process = subprocess.Popen(cmdlst,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
            os.chdir(origin_wkdir)
            run_wait(process, timeout=TIME_OUT)

        except VulScanTimeoutException, e:
            warn_msg = "process [%s] is timeout when scanning %s,terminating..." % (process.pid,target)
            kill_child_processes(process.pid)
            process.kill()
        except Exception,e:
            warn_msg = "%s when scanning %s,quiting..." % (str(e),target
  • TA的每日心情
    开心
    2016-6-9 10:39
  • 签到天数: 79 天

    连续签到: 1 天

    [LV.6]常住居民II

    发表于 2015-12-15 14:13:06 | 显示全部楼层
    大神……
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    站长推荐上一条 /1 下一条