0%

PHP-Audit-Labs

项目源址:https://github.com/hongriSec/PHP-Audit-Labs

题目地址:https://www.ripstech.com/php-security-calendar-2017/

ps:实例里的代码太多了,所以只贴关键代码

Day 1 - Wish List

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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($this->file['name'], $this->whitelist)

我们先查一下in_array()函数的定义和用法:https://www.runoob.com/php/func-array-in-array.html

定义:in_array() 函数搜索数组中是否存在指定的值。

语法:in_array(search,array,type)

参数 描述
search 必需。规定要在数组搜索的值
array 必需。规定要搜索的数组。
type 可选。如果设置该参数为 true,则检查搜索的数据与数组的值的类型是否相同。

说明:如果给定的值 search 存在于数组 array 中则返回 true。如果第三个参数设置为 true,函数只有在元素存在于数组中且数据类型与给定值相同时才返回 true。

如果没有在数组中找到参数,函数返回 false。

注释:如果 search 参数是字符串,且 type 参数设置为 true,则搜索区分大小写。

查阅了in_array()函数的具体用法以后发现源码中使用这个函数的时候没有设置第三个参数为ture,导致攻击者可以通过构造的文件名为1upload.php发生强制类型转换去绕过服务器端的检测。

当在使用 in_array() 函数判断时,会将1upload.php强制转换成数字1,而数字1在 range(1,24)数组中,最终绕过in_array()函数判断,导致任意文件上传漏洞。

实例

先分析一下index.php这个文件,他的主要代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
$sql = "SELECT COUNT(*) FROM users";
$whitelist = array();
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$whitelist = range(1, $row['COUNT(*)']);
}
$id = stop_hack($_GET['id']);
$sql = "SELECT * FROM users WHERE id=$id";

if (!in_array($id, $whitelist)) {
die("id $id is not in whitelist.");
}

首先将用户的id存在$whitelist数组里,然后通过GET方法用户的id,同时使用stop_hack()函数进行过滤,然后再利用in_array()函数来判断用户传入的id参数是否在$whitelist数组中,此处的in_array()函数没有将第三个参数设置为true,导致发生强类型转换,攻击者进行注入。

再来看看config.php文件里的stop_hack()过滤函数

1
2
3
4
5
6
7
8
9
function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}

这里过滤了许多进行sql查询的命令,所以考虑使用updatexml报错注入。

payload

1
http://localhost/index.php?id=1 and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))

1

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

分析

这里使用了php的一个模板引擎Twig,这个题属于XSS漏洞。可以看到,这一段代码有两处过滤,'<a href="">Next slide &raquo;</a>';filter_var($nextSlide, FILTER_VALIDATE_URL);,分别采用了escape和filter_var两种方法过滤。但其实escape是使用php内置的htmlspecialchars函数来实现,所以我们只需要考虑绕过这个函数即可。

定义:htmlspecialchars函数把预定义的字符转换为 HTML 实体。

语法:htmlspecialchars(string,flags,character-set,double_encode)

预定义的字符是:

1
2
3
4
5
>& (& 符号)  ===============  &amp;
>" (双引号) =============== &quot;
>' (单引号) =============== &apos;
>< (小于号) =============== &lt;
>> (大于号) =============== &gt;

定义: filter_var() 函数通过指定的过滤器过滤一个变量。

如果成功,则返回被过滤的数据。如果失败,则返回 FALSE。

语法: filter_var(variable, filter, options)

参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。默认是 FILTER_SANITIZE_STRING。此处用了 FILTER_VALIDATE_URL过滤器来判断是否是一个合法的url
options 可选。规定一个包含标志/选项的关联数组或者一个单一的标志/选项。检查每个过滤器可能的标志和选项。

对于这两处过滤,可以使用javascript伪协议来绕过。

实例

核心代码

1
2
3
4
5
$url = $_GET['url'];
if(isset($url) && filter_var($url, FILTER_VALIDATE_URL)){
$site_info = parse_url($url);
if(preg_match('/sec-redclub.com$/',$site_info['host'])){
exec('curl "'.$site_info['host'].'"', $result);

首先通过GET方法获取一个url参数,之后这个url参数经过了两层过滤,分别是filter_var()函数和parse_url()函数,同时又规定url必须以sec-redclub.com为结尾才能继续,这些条件都满足了以后,程序使用exec()函数来使用$site_info[‘host’]执行拼接curl命令。

此处我们需要绕过filter_var()函数和parse_url()函数,并且满足$site_info[‘host’]的结尾为sec-redclub.com。

绕过filter_var()函数:?url=demo://sec-redclub.com

2

绕过parse_url()函数:?url=demo://"|type=f1agi3hEre.php;||"sec-redclub.com

3

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

分析

第一处漏洞在代码第8行使用了class_exists()函数当调用class_exists()函数时会触发用户定义的__autoload()函数,用于加载找不到的类,这里存在文件包含漏洞。攻击者可以通过路径穿越来包含任意文件,前提是PHP5~5.3版本才可以,例如类名为:../../../../etc/passwd的查找,将查看passwd文件内容。

功能:class_exists会检查是否存在对应的类

语法:bool class_exists ( string $class_name [, bool $autoload ] )

如果由 class_name 所指的类已经定义,此函数返回 TRUE,否则返回 FALSE

第二处漏洞在代码第9行,此处实例化类的类名和传入类的参数均在用户的控制之下,攻击者可以在此处使用PHP的内置类SimpleXMLElement进行XXE攻击,获取目标文件的内容。

实例

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);

首先定义了一个类似_autoload函数为sql_autolad_register函数,作用为输出404。

之后利用class_exists()函数来判断类是否存在,不存在就调用sql_autolad_register函数,并且class_exists传入的参数classname由用户输入的name、param、param2参数决定。

因此,可以利用class_exists函数调用php内置类GlobIterator,来进行文件搜索,payload为?name=GlobIterator&param=./*.php&param2=0

4

我们可以发现藏有flag的文件为f1agi3hEre.php,之后利用XXE漏洞里的SimpleXMLElement类构造payload来读取该文件的内容。此处还需结合php流,当当文件中存在: < > & ‘ “ 这5个符号时,会导致XML文件解析错误,所以我们这里利用PHP文件流,将要读取的文件内容经过 base64编码后输出即可,具体payload为?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=D://phpstudy_pro//WWW//PHP-Audit-Labs//Part1//Day3//f1agi3hEre.php">]><x>%26xxe;</x>&param2=2,最后使用base64解码即可

5

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

分析

该程序通过POST请求两个参数,然后将参数传入loginViaXml()函数,该函数首先通过一个if判断,使用strpos()函数过滤接收参数中的“<”和“>”,然后通过格式化字符串的方式,使用xml结构存储用户登陆信息,最后实例化Login类,并调动login方法进行登陆。查询一下strpos()函数的用法

定义: 查找字符串在另一字符串中第一次出现的位置(区分大小写)

语法: strpos(string,find,start)

参数 描述
string 必需。规定被搜索的字符串。
find 必需。规定要查找的字符。
start 可选。规定开始搜索的位置。

注意: 返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。且字符串位置从 0 开始,不是从 1 开始。

在这道代码审计中,开发者只考虑了strpos()函数返回false的情况,没有考虑匹配到的字符在首位时会返回0的情况,所以我们可以在输入的用户名和密码的首字符注入“<”,从而注入xml数据。

实例

这题在攻防世界做过,当时没有源码,因为存在.git泄露,所以需要利用GitHack得到网站的源码,但这里已经给了源码,可以直接分析,发现问题出现在api.php里的buy函数,这个函数就是用来判断是否猜中

1
2
3
4
5
6
7
8
9
10
11
12
13
function buy($req){
require_registered();
require_min_money(2);

$money = $_SESSION['money'];
$numbers = $req['numbers'];
$win_numbers = random_win_nums();//中奖号码由随机数函数生成
$same_count = 0;
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){//这里用了弱比较==来判断每一位数是否猜中
$same_count++;
}
}

用户的数据是以json格式上传的,如果我们传一个里面包含7个true元素的数组,就可以绕过。例如TRUE,1,”1”都可以绕过。抓包修改参数就可以了

抓包

6

修改参数

7

这样改完之后,当函数在判断号码是否中奖时,每次都会判断ture是否等于某个随机数,这样只要随机数不是0,都将成功判断。

8

这样多发包几次,就可以赢得足够的钱来购买flag了。

9

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

分析

这道题主要考察由php内置函数mail引发的命令执行漏洞,首先查看一下php自带的mail函数的用法

1
2
3
4
5
6
7
bool mail (
string $to , //指定邮件接收者,即接收人
string $subject , //邮件的标题
string $message [, //邮件的正文内容
string $additional_headers [, //指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
string $additional_parameters ]] //指定传递给发送程序sendmail的额外参数
)

在Linux系统上, php 的 mail 函数在底层中已经写好了,默认调用 Linux 的 sendmail 程序发送邮件。而在额外参数( additional_parameters )中, sendmail 主要支持的选项有以下三种

1
2
3
4
5
6
7
8
-O option = value
QueueDirectory = queuedir 选择队列消息

-X logfile
这个参数可以指定一个目录来记录发送邮件时的详细日志情况

-f from email
这个参数可以让我们指定我们发送邮件的邮箱地址

举个例子理解就是,这段代码使用 -X 参数指定日志文件,最终会在 /var/www/html/alice.php 中写入木马

1
2
3
4
5
6
7
8
<?php
$to = 'Alice@example.com';
$subject = 'Hello Alice!';
$message = '<?php phpinfo(); ?>';
$headers = "CC:somebodyelse@example.com";
$options = '-OQueueDirectory=/tmp -X /var/www/html/alice.php';
mail($to, $subject, $headers, $options);
?>
1
2
3
4
5
6
7
17220 <<< To: Alice@example.com
17220 <<< Subject: Hello Alice!
17220 <<< X-PHP-Originating-Script: 0:test.php
17220 <<< CC: somebodyelse@example.com
17220 <<<
17220 <<< <?php phpinfo(); ?>
17220 <<< [EOF]

这道题还涉及了之前的一个filter_var()函数,当时那个题的第二个参数使用了FILTER_VALIDATE_URL过滤器来判断是否是一个合法的url,而这里使用了FILTER_VALIDATE_EMAIL确保只使用有效的电子邮件地址 $email,filter_var($email, FILTER_VALIDATE_EMAIL)

而 filter_var() 问题在于,在双引号中嵌套转义空格仍然能够通过检测。同时由于底层正则表达式的原因,我们通过重叠单引号和双引号,欺骗 filter_val() 使其认为我们仍然在双引号中,这样就可以绕过检测,但是由于引入的特殊符号,虽然绕过了 filter_var() 针对邮箱的检测,但是由于PHP的 mail() 函数在底层实现中,调用了 escapeshellcmd() 函数,对用户输入的邮箱地址进行检测,导致即使存在特殊符号,也会被 escapeshellcmd() 函数处理转义,这样就没办法达到命令执行的目的了 ,之后还有一行return escapeshellarg($email);,主要是为了处理$email传入的数据。

escapeshellarg] — 把字符串转码为可以在 shell 命令里使用的参数

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

语法:string escapeshellarg ( string $arg )

然而 escapeshellcmd() 和 escapeshellarg ()一起使用,会造成特殊字符逃逸,例如

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

首先传入的参数是127.0.0.1' -v -d a=1

由于 escapeshellarg 先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果为'127.0.0.1'\'' -v -d a=1'

接着 escapeshellcmd 函数对第二步处理后字符串中的 \ 以及 a=1’ 中的单引号进行转义处理,结果为'127.0.0.1'\\'' -v -d a=1\'

由于第三步处理之后的 payload 中的 \\ 被解释成了 \ 而不再是转义字符,所以单引号配对连接之后将 payload 分割为三个部分,所以这个payload可以化简为curl 127.0.0.1\ -v -d a=1' ,即向 127.0.0.1\ 发起请求,POST 数据为 a=1'

实例

这个题做不出来,先鸽了吧!