Hu3sky's blog

php-security-calendar-2017

Word count: 4,545 / Reading time: 21 min
2018/08/25 Share

学习网站

https://www.ripstech.com/php-security-calendar-2017/
一个学习代码审计的平台,每一关都是一个函数产生的漏洞,值得学习

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

这是一个判断文件上传的,使用了in_array来检测文件名,whitelist是(1,24)的数字,而上传的文件名也必须是数字比如1,2,3…24,所以说想上传一个webshell.php就不行,我们来看一下in_array函数的定义
bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),文档说明(strict):
在 $haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 TRUE,否则返回 FALSE。但是默认这个参数为False,这里并没有设置strict的值,所以如果我们上传一个文件是3webshell.php,就会自动转换为3,这样,就成功绕过了。

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

这一关题目实际上用的是PHP的一个模板引擎 Twig ,本题考察XSS漏洞,第10行使用了 Twig 模板引擎定义的 escape 过滤器来过滤link,而实际上这里的 escape 过滤器,是用PHP内置函数 htmlspecialchars(https://twig.symfony.com/doc/2.x/filters/escape.html) 来实现的.

link生成的方式为getNexSlideUrl(),这个主要是让我们绕filter_var($nextSlide, FILTER_VALIDATE_URL);,此函数用来检测$nextSlide变量是否是url格式的,也就是我们提交的参数只能是url,例如 http://www.baidu.com 。而提交其他的就返回bool(False)。所以考虑javascript协议

简化代码

1
2
3
4
5
6
7
<?php
$url=filter_var($_GET['url'],FILTER_VALIDATE_URL);
var_dump($url);
$url=htmlspecialchars($url);
var_dump($url);
echo "<a href='$url'>aaaaaaa </a>";
?>

payload为?url=javascript://comment%250aalert(1).

这里的 // 在JavaScript中表示单行注释,所以后面的内容均为注释,那为什么会执行 alert 函数呢?那是因为我们这里用了字符 %0a ,该字符为换行符,所以 alert 语句与注释符 // 就不在同一行,就能执行。当然,这里我们要对 % 百分号编码成 %25 ,因为程序将浏览器发来的payload:javascript://comment%250aalert(1) 先解码成: javascript://comment%0aalert(1) 存储在变量 $url 中(上图第二行代码),然后用户点击aaaaaaa标签链接就会触发 alert 函数。

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

bool class_exists ( string $class_name [, bool $autoload = true ] )检查指定的类是否已定义。参数 ¶
class_name 类名。名字的匹配是不分区大小写的。autoload 是否默认调用 __autoload。所以此题会默认调用__autoload,此题的包含漏洞也出现在这,攻击者可以使用 路径穿越 来包含任意文件,当然使用路径穿越符号的前提是 PHP5~5.3(包含5.3版本)版本 之间才可以。例如类名为: ../../../../etc/passwd 的查找,将查看passwd文件内容
第二个漏洞,在代码第九行,我们发现实例化类的类名和传入类的参数均在用户的控制之下,攻击者可以通过该漏洞,调用PHP代码库的任意构造函数。即使代码本身不包含易受攻击的构造函数,我们也可以使用PHP的内置类 SimpleXMLElement 来进行 XXE 攻击

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

strpos — 查找字符串首次出现的位置,
函数说明
int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )。
参数 haystack 在该字符串中进行查找。
needle 如果 needle 不是一个字符串,那么它将被转换为整型并被视为字符的顺序值。
offset 如果提供了此参数,搜索会从字符串该字符数的起始位置开始统计。 如果是负数,搜索会从字符串结尾指定字符数开始。
strpos 函数返回查找到的子字符串的下标。如果字符串开头就是我们要搜索的目标,则返回下标 0 ;如果搜索不到,则返回 false
所以payload user=<"><injected-tag%20property="&pass=<injected-tag>

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

此题是mali函数导致的代码执行
函数:

1
2
3
4
5
6
7
bool mail (
string $to ,
string $subject ,
string $message [,
string $additional_headers [,
string $additional_parameters ]]
)

to,指定邮件接收者,即接收人
subject,邮件的标题
message,邮件的正文内容
additional_headers,指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
additional_parameters,指定传递给发送程序sendmail的额外参数。

举例

1
2
3
4
5
6
7
8
<?php
$to='hu3sky111@d0g3.cn';
$subject='hello hu3sky';
$message='<?php phpinfo();?>';
$headers='CC:test@d0g3.cn';
$options='-OQueueDirectory=/tmp -X /var/www/html/shell.php';
mail($to,$subject,$message,$headers,$options);
?>

-X 参数指定日志文件。最终会在 /var/www/html/shell.php 中写入如下数据:

1
2
3
4
5
6
7
17220 <<< To: hu3sky111@d0g3.cn
17220 <<< Subject: hello hu3sky
17220 <<< X-PHP-Originating-Script: 0:test.php
17220 <<< CC: test@d0g3.cn
17220 <<<
17220 <<< <?php phpinfo(); ?>
17220 <<< [EOF]

一个小例子

1
2
3
4
5
6
7
8
9
10
<?php
$param="127.0.0.1' -v -d a=1";
$a=escapeshellarg($param);
$b=escapeshellcmd($a);
$cmd="curl ".$b;
var_dump($a)."\n";
var_dump($b)."\n";
var_dump($cmd)."\n";
system($cmd);
?>

mail()函数在底层调用了 escapeshellcmd() 函数对用户输入的邮箱地址进行处理,此题又用了escapeshellarg函数处理,所以可以构造传入的参数为
127.0.0.1' -v -d a=1
由于escapeshellarg()先对单引号转义再在两边加单引号,在对本来的单引号进行转义
'127.0.0.1'\'' -v -d a=1'
接着escapeshellcmd() 函数再对字符串中的\,’进行转义
'127.0.0.1'\\'' -v -d a=1\'
由于第三步处理之后的payload中的 \\ 被解释成了 \ 而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示:
'127.0.0.1' \\ '' -v -d a=1\'
所以这个payload可以简化为 curl 127.0.0.1\ -v -d a=1’ ,即向 127.0.0.1\ 发起请求,POST 数据为 a=1’ 。

Day 6 - 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)的所有符号,按设计者的思路应该是想表示减号。

正则题目

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
// index.php
<?php
include 'f1agi3hEre.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>

一些说明

1
2
3
4
5
6
7
8
9
10
11
12
13
alpha	字母
ascii 0 - 127的ascii字符
blank 空格和水平制表符
cntrl 控制字符
digit 十进制数(same as \d)
graph 打印字符, 不包括空格
lower 小写字母
print 打印字符,包含空格
punct 打印字符, 不包括字母和数字
space 空白字符 (比\s多垂直制表符)
upper 大写字母
word 单词字符(same as \w)
xdigit 十六进制数字

题目中总共有三处正则匹配,我们分别来看一下其对应的含义。第一处的正则 /^[[:graph:]]{12,}$/ 为:匹配到可打印字符12个以上(包含12),^ 号表示必须以某类字符开头,$ 号表示必须以某类字符结尾
第二处正则表达式:

1
2
3
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;

表示字符串中,把连续的符号、数字、大写、小写,作为一段,至少分六段,例如我们输入 H0ng+Ri 则匹配到的子串为 H 0 ng + R i

第三处的正则表达式:

1
2
3
4
5
6
7
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;

表示为输入的字符串至少含有符号、数字、大写、小写中的三种类型。然后题目最后将 $password 与42进行了弱比较。所以我们的payload为:

1
2
password=42.00e+00000
password=420.00000e-1

Day 7 - 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()的变量覆盖
parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量

1
2
3
4
<?php
$var='0';
parse_str($_SERVER['QUERY_STRING']);
echo $var;

test.php?var=1输出就会变成1

所以,我们通过提交类似 config[dbhost]=127.0.0.1 这样类型的数据,这样因此我们可以控制 getUser() 中第5到8行的全局变量 $config 。如果目标存在登陆验证的过程,那么我们就可以通过变量覆盖的方法,远程连接我们自己的mysql服务器,从而绕过这块的登陆验证,进而进行攻击

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

这里是preg_replace的/e参数导致的代码执行
将$replacement 当作代码执行。
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
该函数的第一个和第三个参数都是我们可以控制的,然而这里的第二个参数却固定为 'strtolower("\\1")' 字符串,那这样要如何执行代码呢?
上面的命令执行,相当于 eval(‘strtolower(“\1”);’) 结果,当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义
所以这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}} 。

1
2
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

但GET参数的原因 会把.转换为_ 1
所以 payload : \S*=${phpinfo()}
下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)

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

这应是一个任意文件读取的漏洞,虽然正则将../替换为空,但却没迭代,所以我们可以利用....//绕过

Day 10 - 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()

需要在burpsuit中发包才能看到phpinfo执行

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

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

这题是个XSS,最终我们的输入会拼接到href标签,htmlentities对单引号不会过滤,要求我们的value必须为数字,所以payload/?a'onclick%3dalert(1)//=s.我用的火狐才能弹窗,Chorme浏览器对单引号进行了拦截,会失败。

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

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

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

所以payload:user = '1234567890123456789\' AND password = ' OR 1=1-'

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

此处是一个任意文件写入,虽然id做了rand处理,但get传入一个id参数,可覆盖原来的id,配合get_object_var跟var_export这样就可以在目录下写shell了

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

$\_SERVER['PHP_SELF'],获取当前执行脚本的文件名,与 document root 有关。例如,在地址为 http://example.com/foo/bar.php 的脚本中使用 $\_SERVER['PHP_SELF'] 将得到 /foo/bar.php 。
end()是将数组的内部指针指向最后一个单元,end(/foo/bar.php)就是bar.php
在第五行会进行解码,所以我们传入的时候先编码一次。
最终payload为/http:%252f%252fhu3sky.ooo?redirect。就会跳转到我们提交的网页上http://hu3sky.ooo

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

fsockopen — 打开一个网络连接或者一个Unix套接字连接
array array_map ( callable $callback , array $array1 [, array $... ] )
intval 获取变量的整数值
第八行 cleanInput() 看似对GET,POST,COOKIE都过滤了,但是$_REQUEST请求包含了GET,COOKIE,POST。
又mode()使用的==,可绕过检测。
payload ?mode=1%0a%0dDELETE%20test.file

CATALOG
  1. 1. 学习网站
  2. 2. Day 1 - Wish List
  3. 3. Day 2 - Twig
  4. 4. Day 3 - Snow Flake
  5. 5. Day 4 - False Beard
  6. 6. Day 5 - Postcard
    1. 6.1. 一个小例子
  7. 7. Day 6 - Frost Pattern
    1. 7.1. 正则题目
  8. 8. Day 7 - Bells
  9. 9. Day 8 - Candle
  10. 10. Day 9 - Rabbit
  11. 11. Day 10 - Anticipation
  12. 12. Day 11 - Pumpkin Pie(暂时过)
  13. 13. Day 12 - String Lights
  14. 14. Day 13 - Turkey Baster
  15. 15. Day 14 - Snowman
  16. 16. Day 15 - Sleigh Ride
  17. 17. Day 16 - Poem