深入分析一款簡(jiǎn)單的Github信息泄露爬蟲

大數(shù)據(jù)

作者:grt1stnull

0×01.前言

Github作為一個(gè)代碼托管平臺(tái),有著海量的開源代碼和許多開發(fā)者。在代碼上傳時(shí),有些開發(fā)者缺乏安全意識(shí),會(huì)在不經(jīng)意間泄露自己的密碼或者密鑰。本文以這里為切入點(diǎn),介紹一個(gè)檢索代碼信息的小爬蟲以及在寫爬蟲時(shí)的一些奇技淫巧。

0×02.github信息泄露

正如前言所述,缺乏安全意識(shí)的開發(fā)者會(huì)造成這個(gè)問題。不止web路徑下的.git目錄會(huì)泄露信息,在托管的開源代碼中也會(huì)產(chǎn)生信息泄露。例子很多,比如php連接數(shù)據(jù)庫的配置文件泄露,那么可能數(shù)據(jù)庫帳號(hào)密碼都泄露了,任何人都可以訪問這個(gè)數(shù)據(jù)庫。再比如通向內(nèi)網(wǎng)的帳號(hào)密碼,管理員帳號(hào)密碼乃至ssh密鑰。

api,即應(yīng)用程序編程接口。眾所周知,http是無狀態(tài)協(xié)議,為了將用戶區(qū)分開引進(jìn)了cookie機(jī)制。有許多廠商,提供了api這個(gè)接口供用戶調(diào)取業(yè)務(wù),為了區(qū)分用戶引進(jìn)了token,比如’https://example.com/get?info=xxx&token=xxx‘

而我比較喜歡做的事就是,在github上找api的密鑰。因?yàn)橄啾扰c帳號(hào)密碼,這個(gè)不但泄露的更多,而且也更難以注意察覺,并且我們調(diào)用方便。比如查詢whois信息,子域名檢測(cè)等等,很多安全廠商提供了api接口,所以如果你沒有密鑰,不妨試試這個(gè)github信息泄露的方法。

shodan可能很多安全從業(yè)者都知道,這是一個(gè)很強(qiáng)大的搜索引擎。下文我會(huì)以爬取github上的shodan api密鑰為例子,寫一個(gè)簡(jiǎn)單的小爬蟲。

0×03.github搜索結(jié)果爬取

1.shodan api格式

首先訪問https://developer.shodan.io/api,這是shodan的api文檔,我們可以看到api請(qǐng)求格式為https://api.shodan.io/shodan/host/{ip}?key={YOUR_API_KEY}。之后我們就可以在github上搜索”?https://api.shodan.io/shodan/host/?key=”來看看。

結(jié)果如圖:

大數(shù)據(jù)

可以看到已經(jīng)有人不小心泄露自己的密鑰了,雖然還有很多人沒有。

2.github信息收集

雖然github有提供api,但是對(duì)代碼檢索功能有限制,所以我們這里不使用api。

首先進(jìn)行搜索我們需要有一個(gè)登錄狀態(tài),大家可以注冊(cè)一個(gè)小號(hào),或者是使用大號(hào),這個(gè)沒關(guān)系的。

登錄狀態(tài)我們可以使用cookies,也可以直接登錄,我們這里說直接登錄。

首先F12抓包可以看到整個(gè)登錄流程,即訪問github.com/login,之后將表單的值傳遞給github.com/session。整個(gè)流程非常清晰。

代碼如下:

import requestsheaders = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0','Referer': 'https://github.com/','Host': 'github.com','Upgrade-Insecure-Requests': '1',}payload = {'commit': 'Sign in', 'login': 'xxxxxx@xxxx.xxx',  'password': 'xxxxxx'}r = requests.get("https://github.com/login", headers=headers)_cookies = r.cookiesr = requests.post("https://github.com/session", headers=headers, data=payload,  cookies=_cookies)

如上可不可以呢?仔細(xì)分析整個(gè)流程,實(shí)際上,表單值中還有一個(gè)authenticity_token,我們要先抓取到這個(gè)值,然后傳遞給表單。

抓取函數(shù)如下:

from lxml import etreedef get_token(text):#<input name="authenticity_token" value="Wwc+VXo2iplcjaTzDJwyigClTyZ9FF6felko/X3330UefrKyBT1f/eny1q1qSmEgFfTm0jKv+HW7rQ5hYu84Qw==" type="hidden">    html = etree.HTML(text)    t = html.xpath("http://input[@name='authenticity_token']")    try:        token = t[0].get('value')    except IndexError:        print("[+] Error: can't get login token, exit...")        os.exit()    except Exception as e:        print(e)        os.exit()    #print(token)    return tokenpayload['authenticity_token'] = get_token(r.content)

現(xiàn)在我們代碼還有什么缺點(diǎn)呢,我覺得就是對(duì)cookies的處理不夠優(yōu)雅。requests有一個(gè)神奇的類requests.session(),可以為每次請(qǐng)求保存cookies,并應(yīng)用于下次請(qǐng)求。

在官方文檔我們可以找到http://www.python-requests.org/en/master/user/advanced/#session-objects。

大數(shù)據(jù)

所以這里我們可以使用requests.session對(duì)代碼進(jìn)行優(yōu)化,即:

import requestssession = requests.Session()r = session.get("https://github.com/login", headers=headers)payload['authenticity_token'] = get_token(r.content)r = session.post("https://github.com/session", headers=headers, data=payload)

獲取登錄狀態(tài)后,我們就可以進(jìn)行搜索,之后列舉出信息。

這里采用lxml進(jìn)行信息提取,xpath很簡(jiǎn)單,不多說,代碼如下:

from lxml import etreewords = "https://api.shodan.io/shodan/host/ key="url = ("https://github.com/search?p=1&q=%s&type=Code" % words)r = session.get(url, headers=headers)html = etree.HTML(r.text)block = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']")print("[+] Info: get item: %i" % len(block))codes = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']/div[@class='file-box blob-wrapper']/table[@class='highlight']/tr/td[@class='blob-code blob-code-inner']")nums = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']/div[@class='file-box blob-wrapper']/table[@class='highlight']/tr/td[@class='blob-num']/a")if len(codes) == len(nums):    print("[+] Info: start get data, waiting")    lines = []    strs = None    for i in range(len(nums)):        #print(etree.tostring(codes[i], method='text'))    try:        text = etree.tostring(codes[i], method='text')    except UnicodeEncodeError:        print("[+] UnicodeEncodeError of a result, jump...")        continue    if nums[i].text == '1':        if strs is not None:            lines.append(strs)            strs = text        else:            strs = "%s \\n %s" % (strs, text)            lines.append(strs)    else:        print("[+] Error: wrong number get for codes lines, exit")print("info : %s" % lines)print("total num of info get: %i" % len(lines))

接下來是正則部分,信息抓取下來后,怎么篩選出我們需要的信息呢?靠的就是這里。

因?yàn)槲覀冎纒hodan api以http的形式,把shodan的token帶入了參數(shù)中。簡(jiǎn)單的以get方法為例,可能是?key=xxx&host=xxx、?hosy=xxx&key=xxx&ip=xxx或者?host=xxx&key=xxx等等形式,所以我們構(gòu)造正則為?key=的形式,然后嘗試匹配key的末尾,可能是’、”、&。

代碼如下:

import repattern = re.compile('key=(.*)[&|"|\']')for a in lines:    strs = re.findall(pattern, str(a))    if len(strs) > 0:        #print(strs[0].split('"')[0])        results = strs[0].split('"')[0]        results = results.split('&')[0]        results = results.split('\'')[0]        if results == '':            continue        print(results)

用一個(gè)簡(jiǎn)單的多線程,多爬取幾頁可以看到輸出:

D32FBKHYYqETSf4bIdmurM7xoZA74FnLE48kKXIaCpuKq4nsTJCglvd9o4y8oBni${SHODAN_API_KEY}AR7LzKvBGZNaXlgkYCg4Z9y3x5lEO352%s${PINCH.USERDEFINED.api_key.value}{ShodanAPIKey}{YOUR_API_KEY}%s$SHODAN_API_KEYMFuS0RPXqInMILeWWPFktPp2BOHUZpzF#{SHODAN_API_KEY}D32FBKHYYqETSf4bIdmurM7xoZA74FnL$SHODAN_API_KEYMFuS0RPXqInMILeWWPFktPp2BOHUZpzF#{SHODAN_API_KEY}${PINCH.USERDEFINED.api_key.value}E48kKXIaCpuKq4nsTJCglvd9o4y8oBni{YOUR_API_KEY}%s${SHODAN_API_KEY}AR7LzKvBGZNaXlgkYCg4Z9y3x5lEO352

雖然已經(jīng)有了輸出,但是注意到并不是所有輸出都符合要求,有的甚至只是一個(gè)變量名。

其實(shí)到這里已經(jīng)結(jié)束了,shodan api長(zhǎng)度為32,只要驗(yàn)證長(zhǎng)度就可以得到密鑰了。但是本著精益求精的精神,我們將會(huì)編寫正則表達(dá)式,進(jìn)一步的獲取信息。

從輸出可以看到,除了輸出token,還有%s和變量名稱兩種形式。熟悉python的人可能知道,%s是python中的格式化輸出。

我們首先去除特殊符號(hào)data = re.findall(pattern1, results)[0],之后判斷字符串類型:

if data == 's':    print("python")elif len(data) < 32:    print("value")else:    print(data)

這里我們順利區(qū)分開了python的輸出、值為token的變量和token。

下一步我們嘗試得到變量的值,即token。我們假設(shè)變量的值就在搜索結(jié)果中,即key=value的形式。

由于上一步中已經(jīng)得到了變量名稱data,所以構(gòu)建正則如下:

pattern0 = re.compile("%s[=|:](.*)[\"|']" % data[:6])results = re.findall(pattern0, a.replace(' ',''))if len(results) > 0:    results = results[0].split('\'')[0]    print(results.split('"'))

我們可以獲得輸出比如[‘sys.argv[1]\\n\\n’]。

然后是對(duì)python格式化輸出%s的解析。通常%s格式化輸出為print(“%s” % strs)或者print(“%s,%s” % (strs, strs))的形式。所以構(gòu)建正則如下:

pattern2 = re.compile('%\([\w|\.|,]+')results = re.findall(pattern2, a.replace(' ',''))lists = []for i in results:    i = i.replace('%(', '')    i = i.split(',')    lists.extend(i)    lists = set(lists)

這里我們首先提取變量名稱strs,之后做了去重操作。

既然得到了變量名詞,我們可以仿照上一步,得到變量的值。

再次運(yùn)行結(jié)果如下:

['//api.bintray.com/packages/fooock/maven/jShodan/images/download.svg)](https://bintray.com/fooock/maven/jShodan/_latestVersion)[![AndroidArsenal](https://img.shields.io/badge/Android%20Arsenal-jShodan-brightgreen.svg?style=flat)]( https://android-arsenal.com/details/1/5312)\\n']D32FBKHYYqETSf4bIdmurM7xoZA74FnLD32FBKHYYqETSf4bIdmurM7xoZA74FnLE48kKXIaCpuKq4nsTJCglvd9o4y8oBniAR7LzKvBGZNaXlgkYCg4Z9y3x5lEO352['//developer.shodan.io](https://developer.shodan.io)\\n']MFuS0RPXqInMILeWWPFktPp2BOHUZpzF['//api.shodan.io/shodan/host/search?key=%s&query=hostname:%s&facets={facets}', '%(\\n']MM72AkzHXdHpC8iP65VVEEVrJjp7zkgd['OPTIONAL)],\\n\\n']0fTS2YJPZAOSQHnC7kSEI06LrTg7pPcV

0×04.爬蟲技巧

1.調(diào)試爬蟲

有時(shí)我們寫完爬蟲后,會(huì)發(fā)現(xiàn)結(jié)果并不是我們想要的,我們就想知道中間出了什么問題。

最直觀的,直接輸出代碼print(req.content),或許復(fù)雜一點(diǎn),輸出成html文件:

def see(text):    with open("./t.html", "w") as f:        f.write(text)see(req.content)

大家可能知道,我們會(huì)用burp suite、fiddle來進(jìn)行移動(dòng)端的抓包分析。同樣的,在這里,我們也可以通過代理,對(duì)爬蟲進(jìn)行分析。這里我使用的是burp suite。

大數(shù)據(jù)

大數(shù)據(jù)

我們不僅可以實(shí)時(shí)分析請(qǐng)求,也可以在history里分析請(qǐng)求。

這里以requests為例,我們可以使用代理設(shè)置,官方文檔如圖:

大數(shù)據(jù)

proxies = {'http': 'http://127.0.0.1:8080','https': 'http://127.0.0.1:8080',}r = session.get("https://github.com/login", headers=headers, proxies=proxies)

但是大家應(yīng)該知道,在瀏覽器中使用burp suite對(duì)https進(jìn)行分析的時(shí)候,需要導(dǎo)入證書。因?yàn)閔ttps會(huì)對(duì)證書進(jìn)行驗(yàn)證,而burp suite不屬于可信證書,所以需要導(dǎo)入。但是這里我們?cè)趺磳?dǎo)入證書呢?

很簡(jiǎn)單,只需要簡(jiǎn)單的加一個(gè)參數(shù)verify即可。這個(gè)verify的意思為,不對(duì)證書進(jìn)行驗(yàn)證。

r = session.get("https://github.com/login", headers=headers, verify=False, proxies=proxies)

2.保存狀態(tài)

為了不需要每次都要登錄,我們可以保存cookie到文件,下次直接讀取cookie就好了。代碼如下:

# 從文件讀入cookiewith open('./cookies.txt', 'rb') as f:    cookies = requests.utils.cookiejar_from_dict(pickle.load(f))session.cookies=cookies# 保存cookiewith open('./cookies.txt', 'wb') as f:    pickle.dump(requests.utils.dict_from_cookiejar(session.cookies), f)

0×05.總結(jié)

這次只是以shodan api為例子,提醒大家注意github信息泄露,也給想要爬取github敏感信息的人拋個(gè)磚。不只是shodan api,github上有更多的api等待你去挖掘。只需要改改正則,調(diào)試一下,你也有了自己的api爬取爬蟲。

附錄

代碼如下(也可以訪問這個(gè)私密gist):

#coding:utf-8import requestsimport refrom lxml import etreeimport osimport ioimport pickleimport threadingimport warningswarnings.filterwarnings('ignore')session = requests.Session()headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0' ,'Referer': 'https://github.com/' ,'Host': 'github.com','Upgrade-Insecure-Requests': '1',}payload = {'commit': 'Sign in' , 'login': 'xxxxx@xxx.xxx', 'password' : 'xxxxxx'}proxies = {'http': 'http://127.0.0.1:8080' ,'https': 'http://127.0.0.1:8080' ,}def see(text):with open("./t.html" , "w") as f:f.write(text)def get_token(text):#html = etree.HTML(text)t = html.xpath("http://input[@name='authenticity_token']")try:token = t[0].get('value' )except IndexError:print("[+] Error: can't get login token, exit...")os.exit()except Exception as e:print(e)os.exit()#print(token)return tokendef get_cookie(session):if not os.path.exists("./cookies.txt" ):r = session.get("https://github.com/login" , headers=headers)#, verify=False, proxies=proxies)payload['authenticity_token'] = get_token(r.content)r = session.post("https://github.com/session" , headers=headers, data= payload)#, verify=False, proxies=proxies)#print(r.cookies.get_dict())#see(r.text)else:with open('./cookies.txt' , 'rb') as f:try:cookies = requests.utils.cookiejar_from_dict(pickle.load(f))except TypeError:os.remove("./cookies.txt")return get_cookie(session)session.cookies=cookiesreturn sessiondef search(url, session):r = session.get(url, headers=headers) #, verify=False, proxies=proxies)html = etree.HTML(r.text)block = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']" )#print("[+] Info: get item: %i" % len(block))codes = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']/div[@class='file-box blob-wrapper']/table[@class='highlight']/tr/td[@class='blob-code blob-code-inner']" )nums = html.xpath("http://div[@class='code-list-item col-12 py-4 code-list-item-public ']/div[@class='file-box blob-wrapper']/table[@class='highlight']/tr/td[@class='blob-num']/a" )if len(codes) == len(nums):lines = []strs = Nonefor i in range (len(nums)):#print(etree.tostring(codes[i], method='text'))try:text = etree.tostring(codes[i], method= 'text')except UnicodeEncodeError:#print("UnicodeEncodeError")continueif nums[i].text == '1' :if strs is not None:lines.append(strs)strs = textelse:strs = "%s \n %s" % (strs, text)lines.append(strs)else:print("[+] Error: wrong number get for codes lines, exit")pattern = re.compile('key=(.*)[&|"|\']')pattern1 = re.compile("\w+")pattern2 = re.compile('%([\w|.|,]+')for a in lines:#a = a.replace(' ','')strs = re.findall(pattern, str(a))if len(strs) > 0:results = strs[0].split('"' )[0]results = results.split('&')[ 0]results = results.split('\'')[ 0]if results == '' :continuetry:data = re.findall(pattern1, results)[0]except IndexError:print(results)continueif data == 's' :resulresults = re.findall(pattern2, a.replace(' ', ''))lists = []for i in results:i = i.replace('%(', '' )i = i.split(',')lists.extend(i)lists = set(lists)for i in lists:pattern0 = re.compile("%s=|:[\"|']" % i[:6])results = re.findall(pattern0, a.replace(' ', ''))if len(results) > 0:results = results[0].split('\'' )[0]print(results.split('"'))#print(a)elif len(data) < 32:pattern0 = re.compile("%s=|:[\"|']" % data[:6])results = re.findall(pattern0, a.replace(' ', ''))if len(results) > 0:results = results[0].split('\'' )[0]print(results.split('"'))#print(a)else:print(data)words = "https://api.shodan.io/shodan/host/ key="session = get_cookie(session)threads = []for i in range( 1, 21):url = "https://github.com/search?p= %i&q=%s&type=Code" % (i, words)t=threading.Thread(target = search, args = (url, session))t.start()threads.append(t)for t in threads:t.join()threads = []for i in range( 21, 41):url = "https://github.com/search?p= %i&q=%s&type=Code" % (i, words)t=threading.Thread(target = search, args = (url, session))t.start()threads.append(t)for t in threads:t.join()with open('./cookies.txt' , 'wb') as f:pickle.dump(requests.utils.dict_from_cookiejar(session.cookies), f)

極客網(wǎng)企業(yè)會(huì)員

免責(zé)聲明:本網(wǎng)站內(nèi)容主要來自原創(chuàng)、合作伙伴供稿和第三方自媒體作者投稿,凡在本網(wǎng)站出現(xiàn)的信息,均僅供參考。本網(wǎng)站將盡力確保所提供信息的準(zhǔn)確性及可靠性,但不保證有關(guān)資料的準(zhǔn)確性及可靠性,讀者在使用前請(qǐng)進(jìn)一步核實(shí),并對(duì)任何自主決定的行為負(fù)責(zé)。本網(wǎng)站對(duì)有關(guān)資料所引致的錯(cuò)誤、不確或遺漏,概不負(fù)任何法律責(zé)任。任何單位或個(gè)人認(rèn)為本網(wǎng)站中的網(wǎng)頁或鏈接內(nèi)容可能涉嫌侵犯其知識(shí)產(chǎn)權(quán)或存在不實(shí)內(nèi)容時(shí),應(yīng)及時(shí)向本網(wǎng)站提出書面權(quán)利通知或不實(shí)情況說明,并提供身份證明、權(quán)屬證明及詳細(xì)侵權(quán)或不實(shí)情況證明。本網(wǎng)站在收到上述法律文件后,將會(huì)依法盡快聯(lián)系相關(guān)文章源頭核實(shí),溝通刪除相關(guān)內(nèi)容或斷開相關(guān)鏈接。

2017-10-20
深入分析一款簡(jiǎn)單的Github信息泄露爬蟲
作者:grt1stnull 0×01 前言 Github作為一個(gè)代碼托管平臺(tái),有著海量的開源代碼和許多開發(fā)者。在代碼上傳時(shí),有些開發(fā)者缺乏安全意識(shí),會(huì)在不經(jīng)意間

長(zhǎng)按掃碼 閱讀全文