一直在想为什么sqlmap能成为一款优秀的工具?而我写的却不行?sqlmap究竟有什么特殊之处?今天探索发现带大家寻找sqlmap的一些神秘细节。
bigarray 大数组
当你使用dump命令导出数据的时候,有没有想过即使百万行数据sqlmap也可以导出来?从软件开发的思路来说,为了方便管理数据,会将数据存储在内存中,如果百万条数据存在内存中势必会导致内存爆炸,sqlmap如何处理?
sqlmap内部实现了bigarray
模块lib/core/bigarray.py
,具体原理是将内存数据存储为一个临时文件,要读取时在读取这个文件就行了。具体多大的数据会被缓存下来,sqlmap中有具体的定义BIGARRAY_CHUNK_SIZE
看着挺大,但单位是字节,一个简单的测试可以看到python中一个int占多大的内存(64位)
具体实现原理是,当bigarray存储的数据达到这个长度时,会将数据的内容pickle反序列化成文件,此时bigarray不再存储这些数据,存储这个临时文件名,当需要的时候通过文件名读取在映射到内存中。
HashDB
Hashdb实现在lib/utils/hashdb.py
,用于存储注入时候的各种信息,主要是封装了sqlite3
数据库,因为会在多线程中使用它,所以HashDB内部读写的时候封装了一层线程锁。
经常会有这样一个现象,第一次扫描完后,后面的扫描好像就很快的样子?主要就是它的作用了,它每次初始化的时候都会检查数据库是否保存有相关的信息,如果有,则直接调用,这样就直接获取了前面扫描时的信息,数据库信息,注入点位置等等。
主要读取哪些信息?可以搜索下_resumeHashDBValues
这个函数,作用就是从数据库中恢复数据。
可以看到恢复的数据都赋值给了kb,kb是什么?这引起了我们下面的要说的。
属性字典
上文说道,Kb是什么意思?跳到lib/core/data.py
找到了kb原始的定义
# object to share within function and classes results kb = AttribDict()
AttribDict()
又是什么?,在找到它的定义
继承了python中的dict
,但是可以这样调用
>>> foo = AttribDict() >>> foo.bar = 1 >>> foo.bar
简单来说可以说是自己实现的一种数据结构吧,用它就可以使用类似class的调用了。只需要设置__getattr__
的魔术方法就可以实现。
那么kb
这个变量究竟是干嘛的呢?根据我对sqlmap的观察,应该是用于存储扫描的各种信息。因为python是单例模式,kb就可以用于全局的设置各种变量了。
DNS Cache
为了性能速度的考虑,sqlmap也会将一些IP地址缓存下来,具体实现如下,会在每次初始化的时候运行
def _setDNSCache(): """ Makes a cached version of socket._getaddrinfo to avoid subsequent DNS requests. """ def _getaddrinfo(*args, **kwargs): if args in kb.cache.addrinfo: return kb.cache.addrinfo[args] else: kb.cache.addrinfo[args] = socket._getaddrinfo(*args, **kwargs) return kb.cache.addrinfo[args] if not hasattr(socket, "_getaddrinfo"): socket._getaddrinfo = socket.getaddrinfo socket.getaddrinfo = _getaddrinfo
HOOK了socket.getaddrinfo
,这种方式还是很hacker的~
sqlmap的线程设计
位置在lib/core/threads.py
,里面封装了一个函数runThreads
用于调度线程,具体实现:
# Start the threads for numThread in xrange(numThreads): thread = threading.Thread(target=exceptionHandledFunction, name=str(numThread), args=[threadFunction]) setDaemon(thread) try: thread.start() except Exception, ex: errMsg = "error occurred while starting new thread ('%s')" % ex.message logger.critical(errMsg) break threads.append(thread) # And wait for them to all finish alive = True while alive: alive = False for thread in threads: if thread.isAlive(): alive = True time.sleep(0.1)
不用thread.join()
阻塞线程的原因是thread.join()
的时候无法接收中断的异常KeyboardInterrupt
进入thread.join()
方法内部,实现join的其实是一个Condition,Condition内部实现也是线程锁,具体可以Google。
总之可以想象一下,join使用后,线程会处于一种”锁”的状态,只有当线程结束后才能“唤醒”,所以就接受不到其他的消息了。
如果大家也看过sqlmap源码的话,会发现很多地方调用getCurrentThreadData()
这个函数,这个的意义是什么?
这个函数其实是实例化的类
class _ThreadData(threading.local): """ Represents thread independent data """ def __init__(self): self.reset() def reset(self): """ Resets thread data model """ self.disableStdOut = False self.hashDBCursor = None self.inTransaction = False self.lastCode = None self.lastComparisonPage = None self.lastComparisonHeaders = None self.lastComparisonCode = None self.lastComparisonRatio = None self.lastErrorPage = None self.lastHTTPError = None self.lastRedirectMsg = None self.lastQueryDuration = 0 self.lastPage = None self.lastRequestMsg = None self.lastRequestUID = 0 self.lastRedirectURL = None self.random = random.WichmannHill() self.resumed = False self.retriesCount = 0 self.seqMatcher = difflib.SequenceMatcher(None) self.shared = shared self.validationRun = 0 self.valueStack = []
继承自threading.local
类,看一下这个类的实现
可以看到,操作这个类的属性的时候都会有一把”锁”,所以他是线程安全的。
它的实现有什么用呢?例如有时候你需要根据前一个的运行结果来判断,然而在多线程中操作变量都需要考虑到线程冲突的情况,所以ThreadData
就解决这个问题,用来存储运行过程中的各种信息、数据。
sqlmap request -r 是如何解析的
有的时候,你将从Burpsuite的原始请求数据保存下来,用python sqlmap.py -r xx.txt
来执行,sqlmap是如何解析这些数据的?在写hack-requests的时候,也有这个需求,如何从原始数据直接发包。
全局搜索requestFile
关键词,定位到初始化时候运行的函数_setRequestFromFile()
,跟到解析的函数parseRequestFile()
这个函数的注释是Parses WebScarab and Burp logs and adds results to the target URL list
,支持从WebScarab和Burp logs添加目标。
解析和我想的差不多,反正就会考虑到各种特殊情况,解析最后返回url,method,data,cookie,headers就行了。
payload 内容检测
如果你的注入点在一个post包里面,sqlmap会对post内容的payload进行检测,判断它是soap,json,还是xml,具体定义如下
POST_HINT_CONTENT_TYPES = { POST_HINT.JSON: "application/json", POST_HINT.JSON_LIKE: "application/json", POST_HINT.MULTIPART: "multipart/form-data", POST_HINT.SOAP: "application/soap+xml", POST_HINT.XML: "application/xml", POST_HINT.ARRAY_LIKE: "application/x-www-form-urlencoded; charset=utf-8", } class POST_HINT: SOAP = "SOAP" JSON = "JSON" JSON_LIKE = "JSON-like" MULTIPART = "MULTIPART" XML = "XML (generic)" ARRAY_LIKE = "Array-like"
然后当检测到注入payload是xml或soap时,会将<
替换为>
,<
替换为<
等等…
sqlmap的爬虫
具体文件在lib/utils/crawler.py
sqlmap的爬虫会爬取自定义的深度以及寻找form表单并自动填充缺失的数据加入扫描,在爬虫运行之前,会先收集sitemap.xml的链接。
爬虫逻辑很常规,没有什么特殊的,重点看看是如何解析表单的。
解析表单内置了一个第三方库https://pypi.org/project/ClientForm/
代码很长,懒得看了。。
Keepalive
keepalive也是一个第三方库thirdparty/keepalive/keepalive.py
主要用于使urllib2 支持 HTTP 1.1 和 keepalive,来减轻网络的拥挤。
以前访问每个网页都需要建立三次TCP握手,而这个可以大大减缓TCP握手带来的消耗(只建立一次TCP连接)可以通过—KeepAlive开启,默认是关闭的。
因为写hack-requests的时候也涉及到了连接池,所以看了一下。
Tor
--tor
就可以开启Tor来进行注入了。
关键函数_setTorProxySettings()
首先会根据代理类型检测本地是否开放了这个端口
# Default SOCKS ports used by Tor DEFAULT_TOR_SOCKS_PORTS = (9050, 9150) # Default HTTP ports used by Tor DEFAULT_TOR_HTTP_PORTS = (8123, 8118)
如果开放了端口,则设置conf.proxy,之后的访问请求中都会带上这个代理(针对HTTP代理)。如果是socks代理,通过hook 全局中的socket来实现。
如何检测Tor是否成功?访问一次https://check.torproject.org/ 根据关键词来判断就可以了。
发表评论